@fabrika/proxy-core 0.8.1 → 0.9.0-dev.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.
- package/package.json +9 -8
- package/types/src/authorize.d.ts +233 -0
- package/types/src/cache.d.ts +50 -0
- package/types/src/constants.d.ts +41 -0
- package/types/src/credentials.d.ts +10 -0
- package/types/src/handoff.d.ts +15 -0
- package/types/src/headers.d.ts +112 -0
- package/types/src/iam.d.ts +115 -0
- package/types/src/index.d.ts +16 -0
- package/types/src/log.d.ts +29 -0
- package/types/src/manifest.d.ts +8722 -0
- package/types/src/oauth-resource.d.ts +52 -0
- package/types/src/oauth.d.ts +12 -0
- package/types/src/service.d.ts +47 -0
- package/types/src/verifier.d.ts +22 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Configuration, not caller-supplied URLs. Resource and metadata stay on the protected app origin. */
|
|
3
|
+
export interface OAuthProtectedResource {
|
|
4
|
+
resource: string;
|
|
5
|
+
resourceMetadata: string;
|
|
6
|
+
authorizationServers: string[];
|
|
7
|
+
scopesSupported: string[];
|
|
8
|
+
}
|
|
9
|
+
/** RFC 9728 metadata response; metadata publication does not admit requests to the resource. */
|
|
10
|
+
export interface OAuthProtectedResourceMetadata {
|
|
11
|
+
resource: string;
|
|
12
|
+
authorization_servers: string[];
|
|
13
|
+
scopes_supported: string[];
|
|
14
|
+
bearer_methods_supported: ['header'];
|
|
15
|
+
}
|
|
16
|
+
/** RFC 6750 challenge parameters with RFC 9728 resource metadata discovery. */
|
|
17
|
+
export interface OAuthBearerChallenge {
|
|
18
|
+
resource_metadata: string;
|
|
19
|
+
error?: 'invalid_token' | 'insufficient_scope';
|
|
20
|
+
scope?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const oauthScopesSchema: z.ZodEffects<z.ZodUnknown, string[], unknown>;
|
|
23
|
+
export declare const oauthProtectedResourceSchema: z.ZodEffects<z.ZodObject<{
|
|
24
|
+
resource: z.ZodEffects<z.ZodString, string, string>;
|
|
25
|
+
resourceMetadata: z.ZodEffects<z.ZodString, string, string>;
|
|
26
|
+
authorizationServers: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
|
|
27
|
+
scopesSupported: z.ZodEffects<z.ZodUnknown, string[], unknown>;
|
|
28
|
+
}, "strip", z.ZodTypeAny, {
|
|
29
|
+
resource: string;
|
|
30
|
+
resourceMetadata: string;
|
|
31
|
+
authorizationServers: string[];
|
|
32
|
+
scopesSupported: string[];
|
|
33
|
+
}, {
|
|
34
|
+
resource: string;
|
|
35
|
+
resourceMetadata: string;
|
|
36
|
+
authorizationServers: string[];
|
|
37
|
+
scopesSupported?: unknown;
|
|
38
|
+
}>, {
|
|
39
|
+
resource: string;
|
|
40
|
+
resourceMetadata: string;
|
|
41
|
+
authorizationServers: string[];
|
|
42
|
+
scopesSupported: string[];
|
|
43
|
+
}, {
|
|
44
|
+
resource: string;
|
|
45
|
+
resourceMetadata: string;
|
|
46
|
+
authorizationServers: string[];
|
|
47
|
+
scopesSupported?: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
export declare function parseOAuthProtectedResource(value: unknown): OAuthProtectedResource | null;
|
|
50
|
+
export declare function oauthProtectedResourceMetadata(config: OAuthProtectedResource): OAuthProtectedResourceMetadata;
|
|
51
|
+
export declare function parseOAuthBearerChallenge(value: unknown): OAuthBearerChallenge | null;
|
|
52
|
+
export declare function formatOAuthBearerChallenge(value: OAuthBearerChallenge): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type OAuthProtectedResource, type ProxyApp } from './manifest';
|
|
2
|
+
/**
|
|
3
|
+
* The auth service's RFC 9728 metadata endpoint. Separate from `/verify` because a 2xx from `/verify`
|
|
4
|
+
* admits the request to the app, while metadata is answered by the proxy itself and never forwarded.
|
|
5
|
+
*/
|
|
6
|
+
export declare const OAUTH_METADATA_VERIFY_PATH = "/oauth-metadata";
|
|
7
|
+
/**
|
|
8
|
+
* The entry's protected resource whose metadata URL is this request's host and exact raw pathname. Only an
|
|
9
|
+
* installation entry has one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findOAuthMetadata(app: ProxyApp, url: URL): OAuthProtectedResource | null;
|
|
12
|
+
export declare function oauthMetadataResponse(resource: OAuthProtectedResource, method: string): Response;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `forward_auth` endpoint — a fetch-style `Request → Response` service, written the way
|
|
3
|
+
* `@fabrika/iam` is so the same code runs on Bun, Node and Workers.
|
|
4
|
+
*
|
|
5
|
+
* Caddy's contract (verified against `modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go`):
|
|
6
|
+
* - the subrequest is rewritten to `GET /verify` with the ORIGINAL headers, plus
|
|
7
|
+
* `X-Forwarded-Method` / `X-Forwarded-Uri`, plus reverse_proxy's `X-Forwarded-{For,Proto,Host}`;
|
|
8
|
+
* - a **2xx** response lets the request continue, and each `copy_headers` field is copied onto the
|
|
9
|
+
* UPSTREAM request — but ONLY when non-empty (Caddy guards each copy with a `not vars ""` matcher);
|
|
10
|
+
* - **any other status** is returned to the client verbatim — status, headers AND body, verified
|
|
11
|
+
* against caddy 2.10.2 — so both a 302 to `/auth/login` and a 401 carrying a JSON login envelope
|
|
12
|
+
* reach the caller and neither reaches the app.
|
|
13
|
+
*
|
|
14
|
+
* The two consequences that shape this file:
|
|
15
|
+
* 1. the request being authorized is described ENTIRELY by the forwarded headers. Never look at
|
|
16
|
+
* this request's own method or path — a missing `X-Forwarded-Uri` must deny, not fall back;
|
|
17
|
+
* 2. because an empty copy header is a no-op rather than a delete, the generated Caddy config
|
|
18
|
+
* strips the token header AND the request id from the inbound request BEFORE `forward_auth`
|
|
19
|
+
* runs. Otherwise a client could present its own token header on a `public` path and have it
|
|
20
|
+
* reach the app, or choose the correlation id that lands in IAM's audit trail. Both are handed
|
|
21
|
+
* back on a 2xx, so the edge is the only thing that mints either.
|
|
22
|
+
*/
|
|
23
|
+
import type { TokenCache } from './cache';
|
|
24
|
+
import type { DelegationIssuer, IamGateway } from './iam';
|
|
25
|
+
import { type ProxyLogger } from './log';
|
|
26
|
+
import { type ProxyManifest } from './manifest';
|
|
27
|
+
export interface VerifyServiceConfig {
|
|
28
|
+
manifest: ProxyManifest;
|
|
29
|
+
iam: IamGateway;
|
|
30
|
+
/** IAM's origin — token `iss` and the base of the login bounce. */
|
|
31
|
+
issuer: string;
|
|
32
|
+
/** `null`/omitted disables the token cache. Same decisions, more IAM calls. */
|
|
33
|
+
cache?: TokenCache | null;
|
|
34
|
+
logger?: ProxyLogger;
|
|
35
|
+
/** Liveness probe path on the auth service's own port. Default `/healthz`. */
|
|
36
|
+
healthPath?: string;
|
|
37
|
+
/** Unix seconds. Injectable for tests. */
|
|
38
|
+
now?: () => number;
|
|
39
|
+
/** Issues delegation grants for membership and scope-creating endpoints; see `AuthorizerOptions.delegations`. */
|
|
40
|
+
delegations?: DelegationIssuer | null;
|
|
41
|
+
}
|
|
42
|
+
export type VerifyService = (request: Request) => Promise<Response>;
|
|
43
|
+
/**
|
|
44
|
+
* Build the `/verify` service. Endpoint tables are compiled once here, not per request — the planner is
|
|
45
|
+
* on the hot path of every request to every app.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createVerifyService(config: VerifyServiceConfig): VerifyService;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local token verification — the warm path. Fetch the JWKS once, then verify every token with no
|
|
3
|
+
* network call on the warm path.
|
|
4
|
+
*
|
|
5
|
+
* The verification itself is `@fabrika/jwks`, shared byte-for-byte with the app SDK. What this file
|
|
6
|
+
* owns is the proxy's construction shape: one long-lived process holding one verifier, so the key set
|
|
7
|
+
* is cached per instance, and the audience is the app the forwarded request resolved to — per call,
|
|
8
|
+
* not per verifier.
|
|
9
|
+
*/
|
|
10
|
+
import type { Jwks, TokenAudienceProfile } from '@fabrika/auth-core';
|
|
11
|
+
import { type VerifyResult } from '@fabrika/jwks';
|
|
12
|
+
export type { VerifyResult };
|
|
13
|
+
export declare class TokenVerifier {
|
|
14
|
+
private readonly inner;
|
|
15
|
+
constructor(getJwks: () => Promise<Jwks>, issuer: string, now: () => number, ttlSeconds?: number, refetchCooldownSeconds?: number);
|
|
16
|
+
/**
|
|
17
|
+
* Verify `token` for `audience` (a bare string is an app id). An unknown `kid` forces one JWKS refetch (a key rotated in), then a
|
|
18
|
+
* retry — but at most once per cooldown, because otherwise any caller can turn every request into an
|
|
19
|
+
* IAM round trip just by inventing a `kid`.
|
|
20
|
+
*/
|
|
21
|
+
verify(token: string, audience: string | TokenAudienceProfile): Promise<VerifyResult>;
|
|
22
|
+
}
|