first commit
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# `validate` — tenant-label validation
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides the default RFC-1035-style hostname-label validator
|
||||
(`defaultLabelValidator`) plus two pure string helpers (`normalizeHost`,
|
||||
`stripBaseDomain`) that the resolver uses to take an inbound `Host`
|
||||
header all the way down to a candidate tenant slug. Splitting these
|
||||
helpers out of the resolver body keeps the resolver readable and the
|
||||
host-parsing edge cases unit-testable in isolation.
|
||||
|
||||
Tenant labels are persisted as `tenantId` and compared against by
|
||||
downstream RBAC / row-level filters, so the canonical form is enforced
|
||||
here: lower-case ASCII, 1..63 characters, `[a-z0-9-]`, no leading or
|
||||
trailing hyphen.
|
||||
|
||||
## Features
|
||||
|
||||
- `defaultLabelValidator` — RFC 1035 §2.3.1 LDH-style validator.
|
||||
- `MAX_LABEL_LENGTH` constant exported for downstream schemas.
|
||||
- `normalizeHost(raw)` — strip `:port` suffix, lower-case, refuse the
|
||||
empty / starts-with-`:` cases.
|
||||
- `stripBaseDomain(host, baseDomain)` — tri-state return:
|
||||
`string` (labels), `null` (exact match — no tenant), `undefined`
|
||||
(mismatch — caller decides whether to raise).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultLabelValidator,
|
||||
normalizeHost,
|
||||
stripBaseDomain,
|
||||
} from "@suaveplan/api-tenant-resolver-subdomain/validate";
|
||||
|
||||
const host = normalizeHost("Acme.API.Example.com:443");
|
||||
// → "acme.api.example.com"
|
||||
const labels = stripBaseDomain(host!, "api.example.com");
|
||||
// → "acme"
|
||||
defaultLabelValidator(labels!); // → true
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { defaultLabelValidator } from "@suaveplan/api-tenant-resolver-subdomain/validate";
|
||||
|
||||
const RESERVED = new Set(["www", "admin", "api"]);
|
||||
const customValidator = (label: string): boolean => {
|
||||
if (!defaultLabelValidator(label)) return false;
|
||||
return !RESERVED.has(label);
|
||||
};
|
||||
// → pass `customValidator` as `validate` in SubdomainTenantResolverOptions
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LabelValidator` — `(label: string) => boolean`.
|
||||
- `defaultLabelValidator: LabelValidator`.
|
||||
- `MAX_LABEL_LENGTH: 63`.
|
||||
- `normalizeHost(raw: string | null | undefined): string | null`.
|
||||
- `stripBaseDomain(host, baseDomain): string | null | undefined`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The label validator is hand-coded against the character-code range
|
||||
(`0x30..0x39`, `0x61..0x7a`, `0x2d`) rather than a regex so the
|
||||
hot-path has no allocator overhead. The tri-state `stripBaseDomain`
|
||||
return is the standard "missing vs absent vs malformed" pattern from
|
||||
the rest of the codebase: callers MUST distinguish "host is exactly the
|
||||
apex" (advance the chain — common for marketing pages) from "host has
|
||||
the wrong apex" (strict-mode error). `normalizeHost` does not honour
|
||||
IPv6 bracketed literals (`[::1]`) — tenant resolution by IPv6 host is
|
||||
not a supported deployment shape.
|
||||
Reference in New Issue
Block a user