Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# @suaveplan/api-auth-anonymous — trivial session resolver for @suaveplan/api: always returns null Principal. Used as the tail of resolver chains for fully public APIs and as the foundation conformance seed for every other session-resolver in the stack.
Stage-4 trivial session resolver for `@suaveplan/api`.
Always returns `null` for every request. Use this as the tail of resolver
chains for public endpoints, and as the canonical adapter for fully public
APIs. The package also serves as the foundation conformance seed exercised by
every other concrete session-resolver in Stages 4 / 6 / 8.
<!-- begin:auto-docs -->
## Documentation tree
- [adapter](./src/adapter/adapter.md) — Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous resolver with the minimal adapter metadata t…
- [adapter](./src/adapter/adapter.md) — Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous resolver with the minimal adapter metadata t…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous resolver has no per-request failure modes…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous resolver has no per-request failure modes…
- [resolver](./src/resolver/resolver.md) — Factory for the trivial `SessionResolver` that always returns `null`. This is the canonical tail-of-chain resolver ever…
- [resolver](./src/resolver/resolver.md) — Factory for the trivial `SessionResolver` that always returns `null`. This is the canonical tail-of-chain resolver ever…
<!-- end:auto-docs -->
## Why does "no auth" need its own package?
Per the API foundation design doc §2.3, every host configures a session
resolver — even fully public APIs. Configuring `ANONYMOUS_AUTH_ADAPTER`
explicitly makes the "no auth" choice visible in `host.json` and lets the rest
of the host (RBAC gate, error envelope, OpenAPI emission) operate uniformly:
the principal is simply `null`, never `undefined`.
## Surface
| Subpath | Export |
|----------------|---------------------------------------------------|
| `./resolver` | `createAnonymousResolver(opts?): SessionResolver` |
| `./adapter` | `ANONYMOUS_AUTH_ADAPTER: AuthAdapter` |
| `./error-codes`| `AUTH_ANON_CODES`, `AuthAnonError` |
## Usage
```ts
import { ANONYMOUS_AUTH_ADAPTER } from "@suaveplan/api-auth-anonymous/adapter";
host.registerAuth(ANONYMOUS_AUTH_ADAPTER);
// every request now produces `principal: null`; public routes proceed,
// non-public routes 401 via the host's `publicRoutes` decision.
```
For mixed chains, see `createAnonymousResolver()` + `SessionResolverChain`
in `@suaveplan/api-auth/chain`.
## Conformance
The resolver passes `runAuthSessionConformance` (null-only mode) from
`@suaveplan/api-auth/testing`. See
`src/resolver/resolver.test.ts`.
## Errors
`AUTH_ANON.INVALID_OPTIONS` — thrown only when the factory is handed a
non-object or unknown-key options bag. No per-request error paths exist by
design.

View File

@@ -0,0 +1,76 @@
{
"name": "@suaveplan/api-auth-anonymous",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Stage-4 trivial session resolver for @suaveplan/api: always returns null Principal. Used as the tail of resolver chains for fully public APIs and as the foundation conformance seed for every other session-resolver in the stack.",
"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/resolver/resolver.d.ts",
"default": "./dist/resolver/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"]
}

View File

@@ -0,0 +1,72 @@
# `adapter` — `ANONYMOUS_AUTH_ADAPTER`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous
resolver with the minimal adapter metadata the host needs to wire it into the
per-request pipeline. The adapter omits `securityScheme` on purpose:
`validateAuthAdapter` permits a missing scheme for the anonymous topology so
the emitted OpenAPI document contains no `security` entry on routes — the
correct shape for "no authentication required".
## Features
- Stateless singleton — safe to import once per host process.
- `csrf: false` baked in: anonymous traffic has no session to protect; the
generic CSRF middleware in `@suaveplan/api-auth/csrf-policy` skips routes
whose adapter pins `csrf: false`.
- Validates cleanly against `validateAuthAdapter` — no runtime error at host
registration time despite the omitted security scheme.
## Usage: Basic
```ts
import { ANONYMOUS_AUTH_ADAPTER } from "@suaveplan/api-auth-anonymous/adapter";
import { validateAuthAdapter } from "@suaveplan/api-auth";
const adapter = validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER);
host.registerAuth(adapter);
```
## Usage: Advanced
For a chain that mixes typed resolvers with the anonymous tail, do not use
`ANONYMOUS_AUTH_ADAPTER` directly — assemble a `SessionResolverChain` so the
typed resolvers fire first:
```ts
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const chain = new SessionResolverChain([
jwtResolver,
createAnonymousResolver(),
]);
const adapter = {
resolver: chain,
securityScheme: jwtSecurityScheme, // the typed resolver still emits its scheme
} as const;
```
## API Reference
| Symbol | Kind | Description |
|------------------------------|----------|------------------------------------------------|
| `ANONYMOUS_AUTH_ADAPTER` | const | `AuthAdapter` with anonymous resolver, no scheme, `csrf: false`. |
## Implementation Notes
- The constant is constructed once at module load and reused across requests
— the resolver itself is documented stateless, so a single instance is safe
to fan out across concurrent requests.
- `csrf: false` is deliberate. Anonymous requests carry no session cookie, so
applying CSRF defence would reject legitimate public traffic. Hosts that
want to additionally rate-limit anonymous traffic should layer
`api-rate-limit` (Stage 8) on top — that is not this package's concern.
- Test coverage exercises (a) `validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER)`
returns the adapter unchanged, (b) the embedded resolver still resolves
null, and (c) the singleton survives repeated import without re-allocation.

View File

@@ -0,0 +1,35 @@
import { validateAuthAdapter } from "@suaveplan/api-auth";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { ANONYMOUS_AUTH_ADAPTER } from "./adapter.js";
describe("ANONYMOUS_AUTH_ADAPTER", () => {
it("validates without a securityScheme (anonymous topology)", {
timeout: 5000,
}, () => {
const out = validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER);
expect(out).toBe(ANONYMOUS_AUTH_ADAPTER);
expect(out.securityScheme).toBeUndefined();
});
it("pins csrf:false (no session to protect)", { timeout: 5000 }, () => {
expect(ANONYMOUS_AUTH_ADAPTER.csrf).toBe(false);
});
it("embedded resolver resolves null for arbitrary requests", {
timeout: 5000,
}, async () => {
const result = await ANONYMOUS_AUTH_ADAPTER.resolver.resolve(
new Request("https://example.test/")
);
expect(result).toBeNull();
});
it("dispose on embedded resolver is idempotent", {
timeout: 5000,
}, async () => {
await ANONYMOUS_AUTH_ADAPTER.resolver.dispose();
await expect(
ANONYMOUS_AUTH_ADAPTER.resolver.dispose()
).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,17 @@
import type { AuthAdapter } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "../resolver/resolver.js";
/**
* Canonical `AuthAdapter` composing the anonymous resolver. Omits
* `securityScheme` deliberately — anonymous-only hosts emit an OpenAPI
* document with no `security` entry on routes, and `validateAuthAdapter`
* permits this exact topology (the "no auth configured" host).
*
* Use this constant when wiring a fully public API. For mixed chains (e.g.
* JWT + anonymous tail), call `createAnonymousResolver()` and assemble the
* chain via `SessionResolverChain` instead.
*/
export const ANONYMOUS_AUTH_ADAPTER: AuthAdapter = {
resolver: createAnonymousResolver(),
csrf: false,
};

View File

@@ -0,0 +1,77 @@
# `error-codes` — `AUTH_ANON.*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous
resolver has no per-request failure modes — every request resolves to `null` by
construction. The single `AUTH_ANON.INVALID_OPTIONS` code covers the only thing
that can fail, which is rejecting malformed options handed to
`createAnonymousResolver(opts)` at construction time. Keeping a dedicated
code-namespace per package (rather than re-using `AUTH.*`) means consumers can
disambiguate anonymous-side problems from general session-resolver issues when
multiple resolvers are mounted on the same chain.
## Features
- Defined via `defineCodes("AUTH_ANON", [...])` from `@suaveplan/error` so the
literal value is always `"AUTH_ANON.INVALID_OPTIONS"`.
- `AuthAnonError extends SuaveplanError<AuthAnonErrorCode>` — instance checks
narrow `error.code` to the registry type at the type level.
- Carries `authAnonCode` as a non-discriminating mirror so adapter code can
branch on `err.authAnonCode === AUTH_ANON_CODES.INVALID_OPTIONS` without
re-typing the parent class.
## Usage: Basic
```ts
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
throw new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"createAnonymousResolver: options is not an object"
);
```
## Usage: Advanced
Wrap a parser failure (e.g. zod) and forward its details into the
`context.issues` slot so observability pipelines can render the schema diff:
```ts
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
import z from "zod";
const OptionsSchema = z.object({}).strict();
export function parseOptions(input: unknown): Record<string, never> {
const out = OptionsSchema.safeParse(input);
if (!out.success) {
throw new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"Anonymous resolver options failed schema validation",
{ context: { issues: out.error.issues } }
);
}
return out.data;
}
```
## API Reference
| Symbol | Kind | Description |
|----------------------|----------|------------------------------------------------------------|
| `AUTH_ANON_CODES` | const | `{ INVALID_OPTIONS: "AUTH_ANON.INVALID_OPTIONS" }` |
| `AuthAnonErrorCode` | type | Union of values in `AUTH_ANON_CODES`. |
| `AuthAnonError` | class | `extends SuaveplanError<AuthAnonErrorCode>`. |
## Implementation Notes
- The single code is deliberate. The resolver is documented to never reject a
request; adding speculative codes for "unrecognised credential" or
"principal lookup failed" would be lying about a contract that the
implementation cannot break by construction.
- Tested in `error-codes.test.ts` for both string value and instance shape.

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { AUTH_ANON_CODES, AuthAnonError } from "./error-codes.js";
describe("AUTH_ANON_CODES", () => {
it("exposes the INVALID_OPTIONS literal", { timeout: 5000 }, () => {
expect(AUTH_ANON_CODES.INVALID_OPTIONS).toBe(
"AUTH_ANON.INVALID_OPTIONS"
);
});
it("AuthAnonError mirrors code into authAnonCode", {
timeout: 5000,
}, () => {
const err = new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"bad options"
);
expect(err).toBeInstanceOf(AuthAnonError);
expect(err.code).toBe(AUTH_ANON_CODES.INVALID_OPTIONS);
expect(err.authAnonCode).toBe(AUTH_ANON_CODES.INVALID_OPTIONS);
expect(err.message).toBe("bad options");
});
it("AuthAnonError accepts options bag", { timeout: 5000 }, () => {
const err = new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"with ctx",
{ context: { reason: "not-an-object" } }
);
expect(err.context).toEqual({ reason: "not-an-object" });
});
});

View File

@@ -0,0 +1,38 @@
import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error-code registry for `@suaveplan/api-auth-anonymous`.
*
* The anonymous resolver has effectively no runtime failure modes — every
* request resolves to `null` by design. The single code covers the only thing
* that can fail: invalid factory options supplied at construction time.
*
* Codes are namespaced `AUTH_ANON.*` and produced via `defineCodes` so the
* full literal value is `"AUTH_ANON.INVALID_OPTIONS"`. Consumers branch on
* the constant rather than the raw string.
*/
export const AUTH_ANON_CODES = defineCodes("AUTH_ANON", [
"INVALID_OPTIONS",
] as const);
export type AuthAnonErrorCode =
(typeof AUTH_ANON_CODES)[keyof typeof AUTH_ANON_CODES];
/**
* Concrete error class for the anonymous-auth package. Extends
* {@link SuaveplanError} so `instanceof AuthAnonError` narrows `code` to
* {@link AuthAnonErrorCode}.
*/
export class AuthAnonError extends SuaveplanError<AuthAnonErrorCode> {
public readonly authAnonCode: AuthAnonErrorCode;
public constructor(
code: AuthAnonErrorCode,
message: string,
options?: SuaveplanErrorOptions
) {
super(code, message, options);
this.authAnonCode = code;
}
}

View File

@@ -0,0 +1,11 @@
// Adapter
export { ANONYMOUS_AUTH_ADAPTER } from "./adapter/adapter.js";
// Error codes
export {
AUTH_ANON_CODES,
AuthAnonError,
type AuthAnonErrorCode,
} from "./error-codes/error-codes.js";
// Resolver
export { createAnonymousResolver } from "./resolver/resolver.js";
export type { AnonymousResolverOptions } from "./resolver/resolver.types.js";

View File

@@ -0,0 +1,79 @@
# `resolver` — `createAnonymousResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Factory for the trivial `SessionResolver` that always returns `null`. This is
the canonical tail-of-chain resolver every public-API host configures: it
makes the "no auth" choice explicit in `host.json` and lets the rest of the
host (RBAC gate, error envelope, OpenAPI emission) operate uniformly because
`principal` is `null` not `undefined`. The factory shape exists — rather than
exposing a constant — so future option flags (e.g. `logAnonymousAccess`) can
be threaded in without a breaking surface change.
## Features
- Always resolves `null` — no header inspection, no network I/O, no state.
- Idempotent `dispose()` no-op (resolver holds no resources).
- Strict zod options schema rejects non-object input and unknown keys with
`AUTH_ANON.INVALID_OPTIONS`.
- Returned type is `SessionResolver` from `@suaveplan/api-auth/resolver` so
it composes directly into `SessionResolverChain`.
## Usage: Basic
```ts
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const resolver = createAnonymousResolver();
// Inside a request pipeline:
const principal = await resolver.resolve(request); // → null
await resolver.dispose(); // safe to call again
```
## Usage: Advanced
Compose as the tail of a resolver chain so a JWT-typed resolver runs first
and the anonymous resolver supplies the `null` short-circuit for unrecognised
requests on public routes:
```ts
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const chain = new SessionResolverChain([
jwtResolver, // recognises Bearer tokens, else returns null
apiKeyResolver, // recognises x-api-key, else returns null
createAnonymousResolver(), // tail: always null
]);
const principal = await chain.resolve(request);
// principal is now `Principal | null`. The host applies its `publicRoutes`
// decision: a non-public route 401s; a public route proceeds with null.
```
## API Reference
| Symbol | Kind | Description |
|------------------------------|----------|-------------------------------------------------------|
| `createAnonymousResolver` | function | `(opts?: AnonymousResolverOptions) => SessionResolver`. |
| `AnonymousResolverOptions` | type | Reserved-for-future-flags options bag. |
## Implementation Notes
- The resolver returns `Promise<null>` literally rather than the wider
`Promise<Principal | null>` so type narrowing in tests can use
`expectTypeOf<...>().toEqualTypeOf<Promise<null>>()`. The chain widens it
back to `Principal | null` at the boundary.
- `dispose()` is a `Promise<void>` not just any promise — the conformance
harness asserts the resolved value is `undefined` so this is load-bearing.
- The factory is the public surface (not a const) because future flags must
be addable without a breaking change to call sites. Even today, passing
`{}` is the canonical call.
- No OTel instrumentation is emitted from this resolver. Anonymous-access
observability lives at the host layer (per-request span tag) — adding
counters here would force a foundation-tier package to depend on
`@suaveplan/opentelemetry`, which is undesirable for a hot path.

View File

@@ -0,0 +1,96 @@
import { runAuthSessionConformance } from "@suaveplan/api-auth/testing";
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
import { AUTH_ANON_CODES, AuthAnonError } from "../error-codes/error-codes.js";
import { createAnonymousResolver } from "./resolver.js";
const TARGET = "https://example.test/api";
describe("createAnonymousResolver", () => {
it("resolveSession returns null for every input", {
timeout: 5000,
}, async () => {
const r = createAnonymousResolver();
expect(await r.resolve(new Request(TARGET))).toBeNull();
expect(
await r.resolve(
new Request(TARGET, {
headers: {
authorization: "Bearer x.y.z",
cookie: "sp_session=abc",
},
})
)
).toBeNull();
await r.dispose();
});
it("resolveSession does not throw on weird URLs", {
timeout: 5000,
}, async () => {
const r = createAnonymousResolver();
expect(
await r.resolve(new Request("https://example.test/?q=%FF"))
).toBeNull();
await r.dispose();
});
it("dispose is idempotent", { timeout: 5000 }, async () => {
const r = createAnonymousResolver();
await r.dispose();
await expect(r.dispose()).resolves.toBeUndefined();
});
it("accepts no-arg call", { timeout: 5000 }, () => {
const r = createAnonymousResolver();
expect(typeof r.resolve).toBe("function");
expect(typeof r.dispose).toBe("function");
});
it("accepts explicit empty options", { timeout: 5000 }, () => {
const r = createAnonymousResolver({});
expect(typeof r.resolve).toBe("function");
});
it("rejects non-object options with AUTH_ANON.INVALID_OPTIONS", {
timeout: 5000,
}, () => {
try {
createAnonymousResolver("nope" as never);
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(AuthAnonError);
expect((err as AuthAnonError).code).toBe(
AUTH_ANON_CODES.INVALID_OPTIONS
);
}
});
it("rejects unknown keys with AUTH_ANON.INVALID_OPTIONS", {
timeout: 5000,
}, () => {
try {
createAnonymousResolver({ unknown: true } as never);
throw new Error("expected throw");
} catch (err) {
expect((err as AuthAnonError).code).toBe(
AUTH_ANON_CODES.INVALID_OPTIONS
);
}
});
it("resolve return-type narrows to Promise<null>", {
timeout: 5000,
}, () => {
const r = createAnonymousResolver();
// Type-level assertion: anonymous never produces a principal.
expectTypeOf(r.resolve).returns.resolves.toBeNull();
});
});
runAuthSessionConformance({
label: "anonymous-direct",
factory: () => createAnonymousResolver(),
seedNull: () => new Request(TARGET),
// No principal seed — anonymous never recognises a request.
seedPrincipal: null,
});

View File

@@ -0,0 +1,42 @@
import type { SessionResolver } from "@suaveplan/api-auth";
import z from "zod";
import { AUTH_ANON_CODES, AuthAnonError } from "../error-codes/error-codes.js";
import type { AnonymousResolverOptions } from "./resolver.types.js";
/**
* Empty-object Zod schema. Today the resolver takes no flags; the strict
* schema guarantees future option additions are call-site-visible and rejects
* non-object inputs at the boundary (rule "Zod wired at boundary").
*/
const OptionsSchema = z.object({}).strict().optional();
/**
* Factory for the trivial null-returning {@link SessionResolver}.
*
* The returned resolver always resolves `null` regardless of request shape —
* no header inspection, no network I/O, no internal state. It is the
* canonical tail-of-chain resolver for fully public APIs and the foundation
* conformance seed for every other session-resolver in the stack.
*
* `dispose()` is a no-op (still idempotent) because the resolver holds no
* resources.
*
* @throws {AuthAnonError} `AUTH_ANON.INVALID_OPTIONS` when `opts` is not a
* plain object or contains unknown keys.
*/
export function createAnonymousResolver(
opts?: AnonymousResolverOptions
): SessionResolver {
const parsed = OptionsSchema.safeParse(opts);
if (!parsed.success) {
throw new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"createAnonymousResolver: options failed schema validation",
{ context: { issues: parsed.error.issues } }
);
}
return {
resolve: async (_req: Request): Promise<null> => null,
dispose: async (): Promise<void> => undefined,
};
}

View File

@@ -0,0 +1,12 @@
/**
* Options bag for {@link createAnonymousResolver}.
*
* Reserved for future flags (e.g. `logAnonymousAccess: boolean`). Today the
* shape is empty; passing any non-object value throws
* `AUTH_ANON.INVALID_OPTIONS`. The strict empty-object schema is preserved on
* purpose — it forces callers to update the call site (rather than silently
* pass extra fields through) when a future flag is added.
*/
export interface AnonymousResolverOptions {
readonly _reserved?: never;
}

View File

@@ -0,0 +1,16 @@
/**
* §16 conformance proof — api-auth-anonymous's resolver MUST pass
* `runAuthSessionConformance` from `@suaveplan/api-auth/testing`,
* in "null-only" mode (no `seedPrincipal`) since the anonymous tail
* resolver never returns a recognised principal.
*/
import { runAuthSessionConformance } from "../../../api-auth/src/testing/conformance.js";
import { createAnonymousResolver } from "../../src/resolver/resolver.js";
runAuthSessionConformance({
label: "api-auth-anonymous",
factory: () => createAnonymousResolver(),
seedNull: () => new Request("https://example.test/protected"),
// anonymous resolver never recognises a principal — null-only mode.
seedPrincipal: null,
});

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

@@ -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-anonymous",
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"],
},
},
});

View File

@@ -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,
},
});