@fabrika/proxy-core 0.8.2 → 0.9.0-cloudflare.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The wire names the proxy and its Caddy config agree on. Both sides are generated from this file
3
+ * (`caddy.ts` reads the same constants the service does), so a rename cannot desynchronise them.
4
+ */
5
+ export { CLIENT_ADDRESS_HEADER, DELEGATION_HEADER, ENDPOINT_HEADER, PROXY_TOKEN_HEADER, UNTRUSTED_FORWARD_HEADERS } from '@fabrika/auth-core';
6
+ export declare const FORWARDED_METHOD_HEADER = "X-Forwarded-Method";
7
+ export declare const FORWARDED_URI_HEADER = "X-Forwarded-Uri";
8
+ export declare const FORWARDED_HOST_HEADER = "X-Forwarded-Host";
9
+ export declare const FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
10
+ /** Correlation id, when the edge supplies one; otherwise the proxy generates a UUIDv7. */
11
+ export declare const REQUEST_ID_HEADER = "X-Request-Id";
12
+ /** The auth service's decision endpoint (Caddy's `uri` subdirective). */
13
+ export declare const DEFAULT_VERIFY_PATH = "/verify";
14
+ /**
15
+ * Set on the auth service's 2xx to a preflight it admitted (ADR-0022 "Cross-origin endpoints"). The answer is the
16
+ * preflight's whole answer: the composition returns it to the browser, without the marker, and never dials the app.
17
+ */
18
+ export declare const PREFLIGHT_HEADER = "X-Fabrika-Preflight";
19
+ /**
20
+ * Set on the auth service's 2xx when the request's own `Authorization` goes on to the application: an anonymous webhook
21
+ * row that forwards it. Every composition deletes `Authorization` from an anonymous request without it, and passes the
22
+ * marker on to no one.
23
+ */
24
+ export declare const FORWARD_AUTHORIZATION_HEADER = "X-Fabrika-Forward-Authorization";
25
+ /** Liveness endpoint on the auth service itself — never routed publicly. */
26
+ export declare const DEFAULT_HEALTH_PATH = "/healthz";
27
+ /**
28
+ * Query parameter the generated Caddy route pins the app id with. Explicit beats host-sniffing: the
29
+ * route already knows which app it fronts, so the auth service never has to infer it.
30
+ */
31
+ export declare const APP_QUERY_PARAM = "app";
32
+ /**
33
+ * Query parameter IAM's `/auth/login` takes the return URL in (ADR-0022). The proxy writes it on the
34
+ * login bounce and redacts it out of the access log, where it would re-emit — percent-encoded — a
35
+ * credential that was just stripped from the logged URI.
36
+ */
37
+ export declare const LOGIN_REDIRECT_PARAM = "redirect";
38
+ /**
39
+ * Reserved on every app host beside the handoff callback, and answered before endpoint selection: it clears the app
40
+ * session and sends the browser to IAM's release (ADR-0067 §5). It takes the return path in `LOGIN_REDIRECT_PARAM`.
41
+ */
42
+ export declare const AUTH_SIGNOUT_PATH = "/__fabrika/auth/signout";
43
+ /** IAM's path that revokes the browser's terminal sessions and returns it to the application (ADR-0067 §5). */
44
+ export declare const SIGNOUT_RELEASE_PATH = "/auth/terminals/release";
45
+ /** The JWKS cache TTL and the unknown-`kid` refetch cooldown, owned by the verifier both planes share. */
46
+ export { JWKS_REFETCH_COOLDOWN_SECONDS, JWKS_TTL_SECONDS } from '@fabrika/jwks';
47
+ /** Default IAM request timeout. A hung IAM must deny, not hang every request behind it. */
48
+ export declare const DEFAULT_IAM_TIMEOUT_MS = 5000;
@@ -0,0 +1,10 @@
1
+ /** Read the token out of an `Authorization: Bearer <token>` header. Null when absent/non-bearer. */
2
+ export declare function readBearer(header: string | null): string | null;
3
+ /**
4
+ * Whether an `Authorization` value carries a platform key anywhere: the header itself, or the user or password of Basic
5
+ * credentials. Such a value is never forwarded to an application, whatever its row says: a key sent to a webhook URL
6
+ * by mistake must not reach application code.
7
+ */
8
+ export declare function carriesPlatformKey(header: string, keyPrefix: string): boolean;
9
+ /** Read a single cookie value out of a raw `Cookie` header. Null when absent or malformed. */
10
+ export declare function readCookie(header: string | null, name: string): string | null;
@@ -0,0 +1,20 @@
1
+ export interface HandoffAttempt {
2
+ state: string;
3
+ verifier: string;
4
+ challenge: string;
5
+ }
6
+ /** Create the browser-held verifier and the public values IAM carries through the login. */
7
+ export declare function createHandoffAttempt(): Promise<HandoffAttempt>;
8
+ /**
9
+ * A verifier made for no challenge. IAM consumes a code before it compares the verifier, so redeeming a code with this
10
+ * one only spends the code.
11
+ */
12
+ export declare function unmatchedVerifier(): string;
13
+ /** S256, matching PKCE: the verifier never rides in a URL or reaches IAM through the browser. */
14
+ export declare function handoffChallenge(verifier: string): Promise<string>;
15
+ /** Return the dynamic `__Host-` cookie name for a validated public state. */
16
+ export declare function handoffCookieName(state: string | null): string | null;
17
+ /** Add the public handoff coordinates to IAM's login URL. */
18
+ export declare function setHandoffParams(url: URL, attempt: HandoffAttempt): void;
19
+ /** Keep application cookies, but never expose a Fabrika bearer credential to the upstream app. */
20
+ export declare function stripFabrikaCookies(headers: Headers): void;
@@ -0,0 +1,112 @@
1
+ import type { CspExtensions, EndpointCache } from '@fabrika/auth-core';
2
+ /** Every powerful feature off. ADR-0022 lists the same names; change both together. */
3
+ export declare const PERMISSIONS_POLICY_FEATURES: readonly string[];
4
+ export type HeaderPolicy = readonly (readonly [name: string, value: string])[];
5
+ /** The named origins a contract adds to seven CSP directives: exact `https://` origins, never patterns. */
6
+ export interface CspOrigins {
7
+ readonly connectSrc: readonly string[];
8
+ readonly imgSrc: readonly string[];
9
+ readonly frameAncestors: readonly string[];
10
+ readonly scriptSrc: readonly string[];
11
+ /** `frame-src 'self' <origins>` is emitted only when it names an origin; without it frames fall back to `default-src 'self'`. */
12
+ readonly frameSrc: readonly string[];
13
+ readonly styleSrc: readonly string[];
14
+ readonly fontSrc: readonly string[];
15
+ /**
16
+ * `worker-src 'self' blob:` is emitted only when it holds `blob:`, its one source (a challenge widget that starts its
17
+ * workers from `Blob` URLs); without it workers fall back to `script-src`. Absent from every manifest without it, so a
18
+ * proxy that does not know the directive still parses those.
19
+ */
20
+ readonly workerSrc?: readonly string[];
21
+ }
22
+ /** The one source a contract's `workerSrc` holds: a worker the page's own script built from a `Blob`. */
23
+ export declare const WORKER_BLOB_SOURCE = "blob:";
24
+ /**
25
+ * An exact origin with a literal hostname: no path, no credentials and no wildcard (`new URL` accepts `*` in a
26
+ * hostname), so it means one site and is safe inside a header value. CSP origins are `https:` only.
27
+ */
28
+ export declare function isPolicyOrigin(value: string, protocols?: readonly string[]): boolean;
29
+ /** The most named origins one directive may carry: the contract build and the manifest parser refuse more. */
30
+ export declare const MAX_CSP_ORIGINS = 16;
31
+ export declare const NO_CSP_ORIGINS: CspOrigins;
32
+ export declare function cspOrigins(csp: CspExtensions): CspOrigins;
33
+ /**
34
+ * The policy for one application: the fixed CSP, extended by the contract's named origins and by the Operations
35
+ * ingest origin the browser reports errors to.
36
+ */
37
+ export declare function headerPolicy(origins: CspOrigins, operationsOrigin: string | null): HeaderPolicy;
38
+ /** The browser-facing scheme of a manifest entry's hosts; behind a TLS-terminating balancer no header can supply it. */
39
+ export type PublicScheme = 'http' | 'https';
40
+ /** One year. No `includeSubDomains` and no `preload`: applications share parent domains. */
41
+ export declare const STRICT_TRANSPORT_SECURITY = "max-age=31536000";
42
+ /**
43
+ * What every answer of an application host carries for its transport: HSTS on an `https` host, nothing on `http` (the
44
+ * local stack). Only the proxy knows the scheme, from the manifest, so the framework's in-process policy has none.
45
+ */
46
+ export declare function transportPolicy(scheme: PublicScheme): HeaderPolicy;
47
+ /**
48
+ * The transport headers the policy owns: an `https` entry's policy sets them, and every other answer loses the
49
+ * application's own, so an `http` host never pins a browser to a scheme it does not serve.
50
+ */
51
+ export declare const TRANSPORT_HEADERS: readonly string[];
52
+ /** The policy of a contract application's manifest entry, its transport header included. */
53
+ export declare function appHeaderPolicy(app: {
54
+ readonly csp: CspOrigins;
55
+ readonly operationsOrigin?: string;
56
+ readonly scheme: PublicScheme;
57
+ }): HeaderPolicy;
58
+ /** The policy of an answer that belongs to no contract application: no named origin, no ingest. */
59
+ export declare const BASE_HEADER_POLICY: HeaderPolicy;
60
+ /** Raster formats a browser only ever renders as an image; every other download, SVG and HTML included, is an attachment. */
61
+ export declare const INLINE_FILE_TYPES: readonly string[];
62
+ /** What every answer of a download endpoint carries in place of the policy, its errors included. */
63
+ export declare const FILE_TREATMENT: HeaderPolicy;
64
+ /** The file treatment of a manifest entry's downloads, its transport header included. */
65
+ export declare function appFileTreatment(app: {
66
+ readonly scheme: PublicScheme;
67
+ }): HeaderPolicy;
68
+ /**
69
+ * Headers an application may not send at all: each names a reporting endpoint the policy never approved, and a
70
+ * report-only CSP reports there whatever the enforced one says (`document-uri`, `blocked-uri`).
71
+ */
72
+ export declare const REMOVED_RESPONSE_HEADERS: readonly string[];
73
+ /** The most origins one environment may list for its cross-origin endpoints: Control and the manifest parser refuse more. */
74
+ export declare const MAX_CROSS_ORIGINS = 16;
75
+ /**
76
+ * An origin an environment may list for its cross-origin endpoints: exact, a literal host, `https:` (or `http:`, which
77
+ * only a local host is served over). `null`, an opaque origin, never matches.
78
+ */
79
+ export declare function isCrossOrigin(value: string): boolean;
80
+ /** What a cross-origin page may send beside the safelisted headers: the key, and the JSON body's type. */
81
+ export declare const CROSS_ORIGIN_REQUEST_HEADERS = "Authorization, Content-Type";
82
+ /** How long a browser may reuse an answered preflight, in seconds. */
83
+ export declare const CROSS_ORIGIN_MAX_AGE = "600";
84
+ /**
85
+ * The headers an answer to an admitted cross-origin request carries: the admitted origin, and `Vary: Origin` unless every
86
+ * origin is admitted. Never `Access-Control-Allow-Credentials`: a session does not cross origins.
87
+ */
88
+ export declare function crossOriginHeaders(allowOrigin: string): HeaderPolicy;
89
+ /** A preflight's answer on top of `crossOriginHeaders`: the endpoint's one method, the two headers, the cache lifetime. */
90
+ export declare function preflightHeaders(allowOrigin: string, method: string): HeaderPolicy;
91
+ /** `crossOriginHeaders` over headers that already carry the answer's own policy; `Vary` is added to, never replaced. */
92
+ export declare function applyCrossOriginHeaders(headers: Headers, policy: HeaderPolicy): void;
93
+ /** `null` and anything but one allowlisted value is an attachment; `Headers` joins repeated fields with `, `, so two never match. */
94
+ export declare function fileDisposition(contentType: string | null): 'inline' | 'attachment';
95
+ /** Whatever the handler or an asset server set for these names is replaced; the removed and transport headers are dropped first. */
96
+ export declare function applyHeaderPolicy(headers: Headers, policy: HeaderPolicy): void;
97
+ /**
98
+ * The file treatment over headers a download already carries: the disposition type follows the `Content-Type`
99
+ * exactly, and the parameters after it (the file name) are kept. The proxy passes `appFileTreatment` of its entry.
100
+ */
101
+ export declare function applyFileTreatment(headers: Headers, treatment?: HeaderPolicy): void;
102
+ /**
103
+ * A cacheable row's successful answer (`http.cache`): its `Cache-Control` replaces the `no-store` of the policy or the
104
+ * file treatment. Every other answer, an error or a redirect included, keeps `no-store`.
105
+ */
106
+ export declare function applyAnswerCache(headers: Headers, row: {
107
+ readonly cache?: EndpointCache;
108
+ }, status: number): void;
109
+ /** The `Cache-Control` of a cacheable row's successful answer: `immutable` is reused for a year. */
110
+ export declare function cacheControlOf(cache: EndpointCache): string;
111
+ /** A copy of `response` whose headers `apply` rewrote; status and body are unchanged. */
112
+ export declare function withResponseHeaders(response: Response, apply: (headers: Headers) => void): Response;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The proxy's view of the IAM service: the three cold-path calls it needs, and nothing else.
3
+ *
4
+ * `IamGateway` is a STRUCTURAL SUBSET of `IamRpc`, so on Cloudflare the thin Worker passes its
5
+ * `env.IAM` service binding straight in with no adapter. On Zerops there is no binding, so
6
+ * `HttpIamGateway` speaks HTTP to the global IAM service — the public hop ADR-0022 calls cold-path
7
+ * only: everything after the first exchange is a local JWKS verification against the cached token.
8
+ *
9
+ * FAIL-CLOSED CONTRACT: an unreachable, slow or incoherent IAM raises `IamUnavailableError`. A
10
+ * *decided* negative (`{ ok: false, reason }`) is returned normally. The caller must map the former
11
+ * to a deny too — the distinction only chooses the status, never the outcome.
12
+ */
13
+ import type { ExchangeAuthCodeInput, ExchangeAuthCodeResult, Jwks, MintFromKeyInput, MintFromKeyResult, MintTokenInput, MintTokenResult } from '@fabrika/auth-core';
14
+ /**
15
+ * What the proxy needs of IAM. IAM's entrypoints satisfy it structurally — the CF binding drops
16
+ * straight in with no adapter. Three of the four calls are `IamRpc`; `exchangeAuthCode` is
17
+ * deliberately NOT (`IamHandoffRpc`, ADR-0022), because only the proxy may turn a handoff code into
18
+ * a session and `IamRpc` is held by every SDK consumer.
19
+ */
20
+ export interface IamGateway {
21
+ mintToken(input: MintTokenInput): Promise<MintTokenResult>;
22
+ mintFromKey(input: MintFromKeyInput): Promise<MintFromKeyResult>;
23
+ exchangeAuthCode(input: ExchangeAuthCodeInput): Promise<ExchangeAuthCodeResult>;
24
+ getJwks(): Promise<Jwks>;
25
+ }
26
+ /**
27
+ * Issues the single-use delegation grant a membership or scope-creating endpoint spends (ADR-0022 rule 6). A
28
+ * structural subset of IAM's membership RPC, so its binding drops in like `IamGateway`'s does. `credential` is the
29
+ * token the proxy just minted for this request; the grant is bound to `endpoint` and `requestId`.
30
+ */
31
+ export interface DelegationIssuer {
32
+ requestDelegation(input: DelegationRequest): Promise<{
33
+ ok: true;
34
+ grant: string;
35
+ expiresAt: number;
36
+ } | {
37
+ ok: false;
38
+ reason: string;
39
+ }>;
40
+ }
41
+ export interface DelegationRequest {
42
+ app: string;
43
+ credential: string;
44
+ requestId: string;
45
+ endpoint: string;
46
+ }
47
+ /**
48
+ * IAM could not be consulted — network failure, timeout, non-200, or a body we cannot read. Distinct
49
+ * from a decided denial: we do not know the answer, so the request is denied with 503 rather than
50
+ * being told it is unauthorized.
51
+ *
52
+ * Carries NO cause and no response body: an error object from `fetch` can carry a URL, and a body
53
+ * can carry a token.
54
+ */
55
+ export declare class IamUnavailableError extends Error {
56
+ readonly operation: string;
57
+ constructor(operation: string);
58
+ }
59
+ export interface HttpIamGatewayOptions {
60
+ /** The IAM service origin, e.g. `https://iam.example.com` (trailing slash tolerated). */
61
+ origin: string;
62
+ /** Per-call timeout. A hung IAM must deny, not hang every request behind the proxy. */
63
+ timeoutMs?: number;
64
+ /** Optional `Authorization: Bearer` the proxy authenticates ITSELF to IAM with. */
65
+ key?: string;
66
+ /** Injectable for tests; defaults to the global `fetch`. */
67
+ fetch?: FetchLike;
68
+ }
69
+ /**
70
+ * Just the call shape the gateway uses. Narrower than `typeof fetch` on purpose: the global carries
71
+ * runtime-specific extras (Bun's `preconnect`) that a test double has no business implementing.
72
+ */
73
+ export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
74
+ /**
75
+ * HTTP transport for `IamGateway` — the Zerops path, where there is no service binding.
76
+ *
77
+ * This file and `packages/iam/src/rpc-http.ts` are two halves of one contract; neither may move
78
+ * without the other in the SAME commit. A mismatch fails closed (any non-200 becomes
79
+ * `IamUnavailableError` → a 503 deny) but is very hard to read from the outside.
80
+ *
81
+ * POST {origin}/auth/mint/session {app, session, requestId} -> MintTokenResult
82
+ * POST {origin}/auth/mint/key {app, key, requestId} -> MintFromKeyResult
83
+ * POST {origin}/auth/mint/exchange {app, code, requestId} -> ExchangeAuthCodeResult (ADR-0022)
84
+ * POST {origin}/auth/mint/delegation {app, credential, requestId, endpoint} -> a grant or a denial (rule 6)
85
+ * GET {origin}/.well-known/jwks.json -> Jwks (public)
86
+ *
87
+ * The four POST endpoints are the `/auth/mint/*` PROXY surface, authenticated by IAM's `proxyKey` — the
88
+ * least-privilege split ADR-0022 requires, kept apart from the management `/rpc/*` surface. They
89
+ * answer **200 on any decided outcome, including a denial**: `{ ok: false, reason }` is "IAM answered:
90
+ * no", a non-200 is "IAM could not be consulted". Both deny; only the second is an incident.
91
+ *
92
+ * Credentials always ride in the POST body, never in a URL — a URL ends up in access logs and error
93
+ * strings; a body does not.
94
+ */
95
+ export declare class HttpIamGateway implements IamGateway, DelegationIssuer {
96
+ private readonly origin;
97
+ private readonly timeoutMs;
98
+ private readonly key;
99
+ private readonly fetchImpl;
100
+ constructor(options: HttpIamGatewayOptions);
101
+ mintToken(input: MintTokenInput): Promise<MintTokenResult>;
102
+ mintFromKey(input: MintFromKeyInput): Promise<MintFromKeyResult>;
103
+ exchangeAuthCode(input: ExchangeAuthCodeInput): Promise<ExchangeAuthCodeResult>;
104
+ requestDelegation(input: DelegationRequest): Promise<{
105
+ ok: true;
106
+ grant: string;
107
+ expiresAt: number;
108
+ } | {
109
+ ok: false;
110
+ reason: string;
111
+ }>;
112
+ getJwks(): Promise<Jwks>;
113
+ private post;
114
+ private send;
115
+ }
@@ -0,0 +1,18 @@
1
+ export { Authorizer } from './authorize';
2
+ export type { AuthorizerOptions, Decision, DenyReason, ForwardedRequest, ResolvedApp } from './authorize';
3
+ export { cacheKey, MemoryTokenCache } from './cache';
4
+ export type { CachedToken, TokenCache } from './cache';
5
+ export { APP_QUERY_PARAM, AUTH_SIGNOUT_PATH, CLIENT_ADDRESS_HEADER, DEFAULT_HEALTH_PATH, DEFAULT_IAM_TIMEOUT_MS, DEFAULT_VERIFY_PATH, DELEGATION_HEADER, ENDPOINT_HEADER, FORWARD_AUTHORIZATION_HEADER, FORWARDED_HOST_HEADER, FORWARDED_METHOD_HEADER, FORWARDED_PROTO_HEADER, FORWARDED_URI_HEADER, JWKS_TTL_SECONDS, LOGIN_REDIRECT_PARAM, PREFLIGHT_HEADER, PROXY_TOKEN_HEADER, REQUEST_ID_HEADER, SIGNOUT_RELEASE_PATH, UNTRUSTED_FORWARD_HEADERS, } from './constants';
6
+ export { carriesPlatformKey, readBearer, readCookie } from './credentials';
7
+ export { stripFabrikaCookies } from './handoff';
8
+ export { HttpIamGateway, IamUnavailableError } from './iam';
9
+ export type { DelegationIssuer, DelegationRequest, HttpIamGatewayOptions, IamGateway } from './iam';
10
+ export { consoleLogger, redactPath, redactUrl, silentLogger } from './log';
11
+ export type { LogFields, LogValue, ProxyLogger } from './log';
12
+ export { findOAuthMetadata, OAUTH_METADATA_VERIFY_PATH, oauthMetadataResponse } from './oauth';
13
+ export { errorAnswer, errorPage, FETCH_MODE_HEADER, isPageNavigation, PAGE_CONTENT_TYPE, pageResponse, renderPage } from './pages';
14
+ export type { ErrorPageStatus, PageAction, PageContent } from './pages';
15
+ export { createVerifyService } from './service';
16
+ export type { VerifyService, VerifyServiceConfig } from './service';
17
+ export { TokenVerifier } from './verifier';
18
+ export type { VerifyResult } from './verifier';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Logging, constrained so that secret material cannot reach it.
3
+ *
4
+ * The rule is structural rather than aspirational: a log field is a `string | number | boolean |
5
+ * null`, call sites pass only decision metadata, and the two values that CAN carry a credential —
6
+ * a request path (share-link tokens ride in the path) and a URL — go through `redactPath` /
7
+ * `redactUrl` first. Nothing in this package ever passes a header, a cookie, a token or a caught
8
+ * error object to a logger.
9
+ */
10
+ export type LogValue = string | number | boolean | null;
11
+ export type LogFields = Record<string, LogValue>;
12
+ export interface ProxyLogger {
13
+ info(message: string, fields?: LogFields): void;
14
+ warn(message: string, fields?: LogFields): void;
15
+ error(message: string, fields?: LogFields): void;
16
+ }
17
+ /** Emits one JSON object per line — what Zerops' log collector expects. */
18
+ export declare const consoleLogger: ProxyLogger;
19
+ /** A logger that discards everything — the default, so a library consumer opts IN to output. */
20
+ export declare const silentLogger: ProxyLogger;
21
+ /**
22
+ * Make a path safe to log. Two hazards, both real:
23
+ * - a query string can carry a declared `query` credential (`?pxt=<token>`) — dropped entirely;
24
+ * - a path SEGMENT can be a share-link `px_` credential — replaced with `px_***`.
25
+ * Anything else is preserved, because an unloggable path is an undebuggable proxy.
26
+ */
27
+ export declare function redactPath(pathname: string): string;
28
+ /** Origin + redacted path. Never the query string. */
29
+ export declare function redactUrl(url: URL): string;