Initial Commit
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# `error-codes` — stable error-code registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the namespaced `TENANT_SUBDOMAIN.*` error-code registry plus the
|
||||
concrete `SubdomainTenantResolverError` class for the
|
||||
`@suaveplan/api-tenant-resolver-subdomain` package. Every public failure
|
||||
path in the resolver constructs an error with one of these codes so
|
||||
consumers can branch on a stable string value (e.g. `error.code ===
|
||||
TENANT_SUBDOMAIN_CODES.BASE_DOMAIN_MISMATCH`) and `instanceof
|
||||
SubdomainTenantResolverError` narrows `code` to the union type.
|
||||
|
||||
These codes back The host-side mapping (4xx vs 5xx response
|
||||
shape) and the OTel `outcome` label so an operator dashboarding tenant
|
||||
resolution sees the exact failure mode without parsing free-text
|
||||
messages.
|
||||
|
||||
## Features
|
||||
|
||||
- Five stable codes: `INVALID`, `NOT_FOUND_IN_REGISTRY`,
|
||||
`BASE_DOMAIN_MISMATCH`, `FORWARDED_HOST_UNTRUSTED`, `INVALID_OPTIONS`.
|
||||
- `defineCodes` from `@suaveplan/error` — values are frozen and the
|
||||
namespace prefix (`TENANT_SUBDOMAIN.`) is applied once.
|
||||
- Subclass with public-constructor lint compliance and a redundant
|
||||
`subdomainTenantResolverCode` field for downstream type narrowing.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
SubdomainTenantResolverError,
|
||||
TENANT_SUBDOMAIN_CODES,
|
||||
} from "@suaveplan/api-tenant-resolver-subdomain/error-codes";
|
||||
|
||||
throw new SubdomainTenantResolverError(
|
||||
TENANT_SUBDOMAIN_CODES.INVALID,
|
||||
"Label 'ACME!' contains forbidden characters"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
SubdomainTenantResolverError,
|
||||
TENANT_SUBDOMAIN_CODES,
|
||||
} from "@suaveplan/api-tenant-resolver-subdomain/error-codes";
|
||||
|
||||
try {
|
||||
await resolver.resolve(request);
|
||||
} catch (e) {
|
||||
if (e instanceof SubdomainTenantResolverError) {
|
||||
switch (e.code) {
|
||||
case TENANT_SUBDOMAIN_CODES.BASE_DOMAIN_MISMATCH:
|
||||
return new Response("Wrong domain", { status: 400 });
|
||||
case TENANT_SUBDOMAIN_CODES.FORWARDED_HOST_UNTRUSTED:
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
default:
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `TENANT_SUBDOMAIN_CODES` — frozen record of code constants.
|
||||
- `SubdomainTenantResolverErrorCode` — union of the registered codes.
|
||||
- `SubdomainTenantResolverError` — concrete subclass of
|
||||
`SuaveplanError<SubdomainTenantResolverErrorCode>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is built with `defineCodes("TENANT_SUBDOMAIN", [...] as const)`
|
||||
so the namespace prefix is enforced once and every value is statically
|
||||
typed as `TENANT_SUBDOMAIN.<NAME>`. The error subclass exposes a public
|
||||
constructor (per the api-stack public-constructor lint rule) and carries
|
||||
a `subdomainTenantResolverCode` field so callers that down-cast on the
|
||||
narrow class don't have to re-narrow the inherited `code` field.
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
SubdomainTenantResolverError,
|
||||
TENANT_SUBDOMAIN_CODES,
|
||||
} from "./error-codes.js";
|
||||
|
||||
describe("error-codes", () => {
|
||||
it("registers all five codes with the namespace prefix", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(TENANT_SUBDOMAIN_CODES.INVALID).toBe("TENANT_SUBDOMAIN.INVALID");
|
||||
expect(TENANT_SUBDOMAIN_CODES.NOT_FOUND_IN_REGISTRY).toBe(
|
||||
"TENANT_SUBDOMAIN.NOT_FOUND_IN_REGISTRY"
|
||||
);
|
||||
expect(TENANT_SUBDOMAIN_CODES.BASE_DOMAIN_MISMATCH).toBe(
|
||||
"TENANT_SUBDOMAIN.BASE_DOMAIN_MISMATCH"
|
||||
);
|
||||
expect(TENANT_SUBDOMAIN_CODES.FORWARDED_HOST_UNTRUSTED).toBe(
|
||||
"TENANT_SUBDOMAIN.FORWARDED_HOST_UNTRUSTED"
|
||||
);
|
||||
expect(TENANT_SUBDOMAIN_CODES.INVALID_OPTIONS).toBe(
|
||||
"TENANT_SUBDOMAIN.INVALID_OPTIONS"
|
||||
);
|
||||
});
|
||||
|
||||
it("constructs an error carrying the discriminator code", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const e = new SubdomainTenantResolverError(
|
||||
TENANT_SUBDOMAIN_CODES.INVALID,
|
||||
"label '!' rejected"
|
||||
);
|
||||
expect(e).toBeInstanceOf(SubdomainTenantResolverError);
|
||||
expect(e.code).toBe(TENANT_SUBDOMAIN_CODES.INVALID);
|
||||
expect(e.subdomainTenantResolverCode).toBe(
|
||||
TENANT_SUBDOMAIN_CODES.INVALID
|
||||
);
|
||||
expect(e.message).toContain("label");
|
||||
});
|
||||
|
||||
it("propagates options.cause through SuaveplanError", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const cause = new Error("inner");
|
||||
const e = new SubdomainTenantResolverError(
|
||||
TENANT_SUBDOMAIN_CODES.INVALID_OPTIONS,
|
||||
"outer",
|
||||
{ cause }
|
||||
);
|
||||
expect(e.cause).toBe(cause);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { SuaveplanErrorOptions } from "@suaveplan/error";
|
||||
import { defineCodes, SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Stable error-code registry for `@suaveplan/api-tenant-resolver-subdomain`.
|
||||
*
|
||||
* Every public failure path raises a {@link SubdomainTenantResolverError}
|
||||
* carrying one of these namespaced `TENANT_SUBDOMAIN.*` codes. Consumers
|
||||
* branch on the string value
|
||||
* (e.g. `error.code === TENANT_SUBDOMAIN_CODES.INVALID`).
|
||||
*
|
||||
* Codes:
|
||||
* - `INVALID` — the extracted label fails the configured `validate`
|
||||
* predicate (e.g. empty, contains a `/`, exceeds max length).
|
||||
* - `NOT_FOUND_IN_REGISTRY` — strict mode + a `registry` callback was
|
||||
* configured AND it returned `null` for the extracted slug.
|
||||
* - `BASE_DOMAIN_MISMATCH` — strict mode + the inbound host does not
|
||||
* end with the configured `baseDomain`.
|
||||
* - `FORWARDED_HOST_UNTRUSTED` — strict mode + `X-Forwarded-Host`
|
||||
* arrived from an upstream IP outside `trustedProxyCidrs`.
|
||||
* - `INVALID_OPTIONS` — `createSubdomainTenantResolver` called with
|
||||
* options that fail Zod validation at construction time.
|
||||
*/
|
||||
export const TENANT_SUBDOMAIN_CODES = defineCodes("TENANT_SUBDOMAIN", [
|
||||
"INVALID",
|
||||
"NOT_FOUND_IN_REGISTRY",
|
||||
"BASE_DOMAIN_MISMATCH",
|
||||
"FORWARDED_HOST_UNTRUSTED",
|
||||
"INVALID_OPTIONS",
|
||||
] as const);
|
||||
|
||||
export type SubdomainTenantResolverErrorCode =
|
||||
(typeof TENANT_SUBDOMAIN_CODES)[keyof typeof TENANT_SUBDOMAIN_CODES];
|
||||
|
||||
/**
|
||||
* Concrete error class for the api-tenant-resolver-subdomain package.
|
||||
* Extends {@link SuaveplanError} so
|
||||
* `instanceof SubdomainTenantResolverError` narrows `code` to
|
||||
* {@link SubdomainTenantResolverErrorCode}.
|
||||
*/
|
||||
export class SubdomainTenantResolverError extends SuaveplanError<SubdomainTenantResolverErrorCode> {
|
||||
public readonly subdomainTenantResolverCode: SubdomainTenantResolverErrorCode;
|
||||
|
||||
public constructor(
|
||||
code: SubdomainTenantResolverErrorCode,
|
||||
message: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.subdomainTenantResolverCode = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
SubdomainTenantResolverError,
|
||||
type SubdomainTenantResolverErrorCode,
|
||||
TENANT_SUBDOMAIN_CODES,
|
||||
} from "./error-codes.js";
|
||||
Reference in New Issue
Block a user