@lunora/cloudflare-access 1.0.0-alpha.8 → 1.0.0-alpha.80

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,256 @@
1
+ import { JWTPayload, JWTVerifyGetKey, KeyObject } from 'jose';
2
+ /**
3
+ * The subset of the Cloudflare `ExecutionContext` the Lunora worker entry and
4
+ * the framework mount seams rely on — `waitUntil` for fire-and-forget work that
5
+ * must outlive the response, and `passThroughOnException` for the top-level
6
+ * error posture.
7
+ *
8
+ * It is deliberately **not** a package. `@lunora/runtime` is the leaf server
9
+ * runtime and `@lunora/nuxt` is a framework integration that intentionally does
10
+ * not depend on `@lunora/runtime`'s worker types, yet both need this exact
11
+ * shape: the runtime to build/forward the worker `fetch`, Nuxt to forward an
12
+ * inbound request to the user's composed worker. Each imports this file by
13
+ * relative path and the bundler (packem/rollup) inlines it: no runtime
14
+ * dependency edge is created, the helper is duplicated only in emitted output,
15
+ * never in source. One source of truth, zero deps. See AGENTS.md → "Top-level
16
+ * `shared/` — bundler-inlined source".
17
+ *
18
+ * Both methods are **optional**: a real Cloudflare `ExecutionContext` always
19
+ * supplies them, but a host that mounts Lunora as a sub-handler (Nitro/H3, a
20
+ * non-Cloudflare preview, a unit test) may hand over a partial context or none
21
+ * at all. Callers therefore invoke them defensively (`ctx.waitUntil?.(…)`) or
22
+ * fall back to {@link NOOP_EXECUTION_CONTEXT}.
23
+ */
24
+ interface ExecutionContextLike {
25
+ /**
26
+ * Present only when Cloudflare Access authenticated the request against a
27
+ * policy attached to the **Worker** (rather than to a hostname). `undefined`
28
+ * on every unauthenticated request, so its presence is itself the "Access
29
+ * authorized this caller" signal — see {@link AccessContextLike}.
30
+ */
31
+ access?: AccessContextLike;
32
+ cache?: {
33
+ purge: (options: {
34
+ purgeEverything?: boolean;
35
+ tags?: string[];
36
+ }) => Promise<unknown>;
37
+ };
38
+ passThroughOnException?: () => void;
39
+ waitUntil?: (promise: Promise<unknown>) => void;
40
+ }
41
+ /**
42
+ * The identity Cloudflare Access attaches to a Worker-protected request.
43
+ *
44
+ * Shape follows the Access application-token payload: `sub` is the stable per-user
45
+ * id, `email` the verified address, `common_name` the service-token name (machine
46
+ * callers, whose `sub` is empty), and `exp` the credential expiry in epoch
47
+ * **seconds**. Group membership is whatever the Access policy emits — a list of
48
+ * names, or of `{ id, name }` objects — hence `unknown`; normalize before use.
49
+ *
50
+ * Cloudflare may add further fields, so the index signature keeps them rather
51
+ * than dropping them: this is a view of a payload we do not own.
52
+ */
53
+ interface AccessIdentityLike {
54
+ [claim: string]: unknown;
55
+ /** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
56
+ common_name?: string;
57
+ /** Verified user email. Present for interactive (SSO) callers. */
58
+ email?: string;
59
+ /** Credential expiry, epoch **seconds**. */
60
+ exp?: number;
61
+ /** IdP group membership — names or `{ id, name }` objects, depending on the policy. */
62
+ groups?: unknown;
63
+ /** Display name from the identity provider, when it emits one. */
64
+ name?: string;
65
+ /** Stable per-user id, and what consumers key a user on. Empty for service tokens. */
66
+ sub?: string;
67
+ /** Cloudflare's per-user UUID. Carried through, but deliberately not used as an id — only this path emits it, so keying on it would not match the JWT path. */
68
+ user_uuid?: string;
69
+ }
70
+ /**
71
+ * The `ctx.access` facade Cloudflare exposes on a Worker protected by Access.
72
+ *
73
+ * Reading the identity from here is preferable to verifying the
74
+ * `Cf-Access-Jwt-Assertion` header: the platform has already authenticated the
75
+ * caller, so there is no JWKS fetch, no audience check to get wrong, and nothing
76
+ * a request can forge — the field simply does not exist unless Access authorized
77
+ * the call. The header path remains the fallback for hostname-scoped Access
78
+ * applications, which do not populate this.
79
+ */
80
+ interface AccessContextLike {
81
+ getIdentity: () => AccessIdentityLike | null | undefined | Promise<AccessIdentityLike | null | undefined>;
82
+ }
83
+ /**
84
+ * The verified claims of a Cloudflare Access caller — from the
85
+ * `Cf-Access-Jwt-Assertion` JWT (hostname-scoped Access applications) or from the
86
+ * platform-supplied `ctx.access.getIdentity()` (Access policies attached to the
87
+ * Worker). Both paths produce this one shape, so nothing downstream branches on
88
+ * how the caller was authenticated.
89
+ *
90
+ * Extends the standard `JWTPayload` (`iss`/`aud`/`sub`/`exp`/`iat`/…) with the
91
+ * Access-specific fields. Which optional fields are present depends on the
92
+ * caller and the Access application config. SSO users carry `email` (and
93
+ * `groups` when the policy emits them), with `sub` as the stable user id.
94
+ * Service tokens carry `common_name` and an empty `sub`; there is no `email`.
95
+ *
96
+ * Cloudflare may add further custom claims — they pass through verbatim via the
97
+ * index signature so the claims stay a faithful view of the identity.
98
+ */
99
+ interface AccessClaims extends JWTPayload {
100
+ /** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
101
+ common_name?: string;
102
+ /** ISO-3166-1 alpha-2 country the request was authorized from, when available. */
103
+ country?: string;
104
+ /** Verified user email. Present for interactive (SSO) callers. */
105
+ email?: string;
106
+ /** Identity-provider group memberships, when the Access policy is configured to emit them. */
107
+ groups?: string[];
108
+ /** Per-session nonce Cloudflare rotates on re-authentication. */
109
+ identity_nonce?: string;
110
+ /** Display name from the identity provider. Populated on the platform-supplied identity, not on the JWT. */
111
+ name?: string;
112
+ /** Token kind, e.g. `"app"`. */
113
+ type?: string;
114
+ /** Cloudflare's per-user UUID. Populated on the platform-supplied identity, not on the JWT — so `userId` is deliberately never derived from it. */
115
+ user_uuid?: string;
116
+ }
117
+ /**
118
+ * The minimal `resolveIdentity` return contract shared with `@lunora/runtime`'s
119
+ * `WorkerOptions.resolveIdentity` (`ResolvedIdentity`). Declared structurally so
120
+ * this package takes no runtime dependency on `@lunora/runtime`; the value is
121
+ * assignable to the runtime hook.
122
+ *
123
+ * `userId` becomes `ctx.auth.userId`; every other key is forwarded (server-side,
124
+ * unforgeable) into `x-lunora-identity` and surfaced via `ctx.auth.getIdentity()`.
125
+ * `exp` (JWT epoch **seconds**) drives WebSocket credential expiry — omit it and
126
+ * a live subscription socket never expires.
127
+ */
128
+ interface ResolvedIdentityLike {
129
+ /** All other claims pass through into `ctx.auth.getIdentity()`. */
130
+ [claim: string]: unknown;
131
+ /** JWT `exp` in epoch **seconds** (NOT milliseconds). Drives WS socket expiry. */
132
+ exp?: number;
133
+ /** Absolute expiry in epoch **milliseconds**. Alternative to `exp`; takes precedence in the runtime. */
134
+ expiresAtMs?: number;
135
+ /** The stable caller id. Becomes `ctx.auth.userId` and what `serverDefault(({auth}) => auth.userId)` stamps. */
136
+ userId: string;
137
+ }
138
+ /**
139
+ * The verified Access identity produced by `createAccessResolver`. A
140
+ * {@link ResolvedIdentityLike} with the commonly-used Access claims promoted to
141
+ * named, camelCased fields (so policies read `auth.identity.groups` etc.) plus
142
+ * the full raw claim set under `access` for fidelity.
143
+ */
144
+ interface ResolvedAccessIdentity extends ResolvedIdentityLike {
145
+ /** The full, verified claim set (snake_cased wire names preserved). */
146
+ access: AccessClaims;
147
+ /** Service-token name (`common_name`), for machine callers. */
148
+ commonName?: string;
149
+ /** Verified email, for SSO callers. */
150
+ email?: string;
151
+ /** IdP group memberships, when emitted by the Access policy. */
152
+ groups?: string[];
153
+ }
154
+ /**
155
+ * A key source for `verifyAccessJwt`. Either a `jose` remote/local JWKS getter,
156
+ * or a single public key (handy for tests that mint their own RS256 tokens).
157
+ * When omitted, a cached remote JWKS is built from `teamDomain`.
158
+ */
159
+ type AccessKeySet = CryptoKey | JWTVerifyGetKey | KeyObject | Uint8Array;
160
+ /** Options for `verifyAccessJwt`. */
161
+ interface VerifyAccessJwtOptions {
162
+ /**
163
+ * The Access application **AUD tag(s)** (the application audience from the
164
+ * Access app's Overview). Verification rejects a token whose `aud` does not
165
+ * include one of these — this is what scopes a token to *your* app.
166
+ */
167
+ aud: string | string[];
168
+ /** Clock-skew tolerance in **seconds** applied to `exp`/`nbf`/`iat`. Default `0`. */
169
+ clockToleranceSec?: number;
170
+ /**
171
+ * Override the verification key source. Primarily for tests; in production
172
+ * leave unset to use the cached remote JWKS derived from `teamDomain`.
173
+ */
174
+ keySet?: AccessKeySet;
175
+ /**
176
+ * Your Cloudflare Access team domain. Accepts the short team name (`acme`),
177
+ * the host (`acme.cloudflareaccess.com`), or a full URL
178
+ * (`https://acme.cloudflareaccess.com`). Determines both the expected issuer
179
+ * and the JWKS endpoint.
180
+ */
181
+ teamDomain: string;
182
+ }
183
+ /**
184
+ * Common options for the request-driven Access primitives — how to read the JWT
185
+ * off the request and what to do when verification fails. Shared by
186
+ * {@link CreateAccessResolverOptions} and `AccessAdminGateOptions`, which add
187
+ * their distinct mapping / authorization step on top.
188
+ */
189
+ interface RequestVerifyOptions extends VerifyAccessJwtOptions {
190
+ /**
191
+ * Cookie name carrying the Access JWT when the header is absent (browser
192
+ * navigations). Default `"CF_Authorization"`.
193
+ */
194
+ cookieName?: string;
195
+ /**
196
+ * Request header carrying the Access JWT. Default `"cf-access-jwt-assertion"`
197
+ * (matched case-insensitively).
198
+ */
199
+ headerName?: string;
200
+ /**
201
+ * Invoked when a token is present but fails verification (bad signature,
202
+ * wrong audience, expired, …). The caller still fails closed (resolver
203
+ * returns `null`, admin gate returns `false`); this is your hook to
204
+ * log/observe. It is **not** called when no token is present at all.
205
+ */
206
+ onError?: (error: unknown, request: Request) => void;
207
+ }
208
+ /**
209
+ * {@link RequestVerifyOptions} with the JWT-verification config made optional,
210
+ * for the primitives that can also authenticate off the platform-supplied
211
+ * identity (`ctx.access`) and therefore may legitimately be given no JWT config
212
+ * at all.
213
+ *
214
+ * The two fields are **all-or-nothing**: supply both to enable the
215
+ * `Cf-Access-Jwt-Assertion` fallback (needed for hostname-scoped Access
216
+ * applications, which do not populate `ctx.access`), or neither to run
217
+ * platform-identity-only. Supplying exactly one throws at construction — that is
218
+ * always a misconfiguration (classically an unset `env.CF_ACCESS_AUD`), and
219
+ * silently degrading it to "no JWT fallback" would turn a broken deployment into
220
+ * a quietly anonymous one.
221
+ */
222
+ interface AccessJwtFallbackOptions extends Omit<RequestVerifyOptions, "aud" | "teamDomain"> {
223
+ /**
224
+ * The Access application **AUD tag(s)**. Required together with `teamDomain`
225
+ * to enable JWT verification; omit both to authenticate only off the
226
+ * platform-supplied identity.
227
+ */
228
+ aud?: string | string[];
229
+ /**
230
+ * Your Cloudflare Access team domain. Required together with `aud` to enable
231
+ * JWT verification; omit both to authenticate only off the platform-supplied
232
+ * identity.
233
+ */
234
+ teamDomain?: string;
235
+ }
236
+ /** Options for `createAccessResolver`; extends {@link AccessJwtFallbackOptions}. */
237
+ interface CreateAccessResolverOptions extends AccessJwtFallbackOptions {
238
+ /**
239
+ * Remap verified claims into the resolved identity. Return an object to
240
+ * shallow-merge over the defaults; return a `userId` to override the derived
241
+ * caller id. Runs only after signature/issuer/audience/expiry are verified.
242
+ */
243
+ mapClaims?: (claims: AccessClaims) => Record<string, unknown>;
244
+ }
245
+ /**
246
+ * A `resolveIdentity`-shaped function: maps an inbound request to a verified
247
+ * identity (or `null` for anonymous). Assignable to `@lunora/runtime`'s
248
+ * `WorkerOptions.resolveIdentity`.
249
+ *
250
+ * The third argument is the request's `ExecutionContext`, which the runtime
251
+ * forwards so a resolver can read the identity Cloudflare Access attaches to a
252
+ * Worker-protected request (`context.access`). It is `undefined` on paths that
253
+ * have no context to give, so a resolver must handle its absence.
254
+ */
255
+ type ResolveIdentityFunction = (request: Request, env?: unknown, context?: ExecutionContextLike) => (ResolvedIdentityLike | null) | Promise<ResolvedIdentityLike | null>;
256
+ export { AccessClaims as A, CreateAccessResolverOptions as C, ExecutionContextLike as E, ResolveIdentityFunction as R, VerifyAccessJwtOptions as V, AccessJwtFallbackOptions as a, AccessKeySet as b, ResolvedAccessIdentity as c, ResolvedIdentityLike as d };
@@ -0,0 +1,256 @@
1
+ import { JWTPayload, JWTVerifyGetKey, KeyObject } from 'jose';
2
+ /**
3
+ * The subset of the Cloudflare `ExecutionContext` the Lunora worker entry and
4
+ * the framework mount seams rely on — `waitUntil` for fire-and-forget work that
5
+ * must outlive the response, and `passThroughOnException` for the top-level
6
+ * error posture.
7
+ *
8
+ * It is deliberately **not** a package. `@lunora/runtime` is the leaf server
9
+ * runtime and `@lunora/nuxt` is a framework integration that intentionally does
10
+ * not depend on `@lunora/runtime`'s worker types, yet both need this exact
11
+ * shape: the runtime to build/forward the worker `fetch`, Nuxt to forward an
12
+ * inbound request to the user's composed worker. Each imports this file by
13
+ * relative path and the bundler (packem/rollup) inlines it: no runtime
14
+ * dependency edge is created, the helper is duplicated only in emitted output,
15
+ * never in source. One source of truth, zero deps. See AGENTS.md → "Top-level
16
+ * `shared/` — bundler-inlined source".
17
+ *
18
+ * Both methods are **optional**: a real Cloudflare `ExecutionContext` always
19
+ * supplies them, but a host that mounts Lunora as a sub-handler (Nitro/H3, a
20
+ * non-Cloudflare preview, a unit test) may hand over a partial context or none
21
+ * at all. Callers therefore invoke them defensively (`ctx.waitUntil?.(…)`) or
22
+ * fall back to {@link NOOP_EXECUTION_CONTEXT}.
23
+ */
24
+ interface ExecutionContextLike {
25
+ /**
26
+ * Present only when Cloudflare Access authenticated the request against a
27
+ * policy attached to the **Worker** (rather than to a hostname). `undefined`
28
+ * on every unauthenticated request, so its presence is itself the "Access
29
+ * authorized this caller" signal — see {@link AccessContextLike}.
30
+ */
31
+ access?: AccessContextLike;
32
+ cache?: {
33
+ purge: (options: {
34
+ purgeEverything?: boolean;
35
+ tags?: string[];
36
+ }) => Promise<unknown>;
37
+ };
38
+ passThroughOnException?: () => void;
39
+ waitUntil?: (promise: Promise<unknown>) => void;
40
+ }
41
+ /**
42
+ * The identity Cloudflare Access attaches to a Worker-protected request.
43
+ *
44
+ * Shape follows the Access application-token payload: `sub` is the stable per-user
45
+ * id, `email` the verified address, `common_name` the service-token name (machine
46
+ * callers, whose `sub` is empty), and `exp` the credential expiry in epoch
47
+ * **seconds**. Group membership is whatever the Access policy emits — a list of
48
+ * names, or of `{ id, name }` objects — hence `unknown`; normalize before use.
49
+ *
50
+ * Cloudflare may add further fields, so the index signature keeps them rather
51
+ * than dropping them: this is a view of a payload we do not own.
52
+ */
53
+ interface AccessIdentityLike {
54
+ [claim: string]: unknown;
55
+ /** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
56
+ common_name?: string;
57
+ /** Verified user email. Present for interactive (SSO) callers. */
58
+ email?: string;
59
+ /** Credential expiry, epoch **seconds**. */
60
+ exp?: number;
61
+ /** IdP group membership — names or `{ id, name }` objects, depending on the policy. */
62
+ groups?: unknown;
63
+ /** Display name from the identity provider, when it emits one. */
64
+ name?: string;
65
+ /** Stable per-user id, and what consumers key a user on. Empty for service tokens. */
66
+ sub?: string;
67
+ /** Cloudflare's per-user UUID. Carried through, but deliberately not used as an id — only this path emits it, so keying on it would not match the JWT path. */
68
+ user_uuid?: string;
69
+ }
70
+ /**
71
+ * The `ctx.access` facade Cloudflare exposes on a Worker protected by Access.
72
+ *
73
+ * Reading the identity from here is preferable to verifying the
74
+ * `Cf-Access-Jwt-Assertion` header: the platform has already authenticated the
75
+ * caller, so there is no JWKS fetch, no audience check to get wrong, and nothing
76
+ * a request can forge — the field simply does not exist unless Access authorized
77
+ * the call. The header path remains the fallback for hostname-scoped Access
78
+ * applications, which do not populate this.
79
+ */
80
+ interface AccessContextLike {
81
+ getIdentity: () => AccessIdentityLike | null | undefined | Promise<AccessIdentityLike | null | undefined>;
82
+ }
83
+ /**
84
+ * The verified claims of a Cloudflare Access caller — from the
85
+ * `Cf-Access-Jwt-Assertion` JWT (hostname-scoped Access applications) or from the
86
+ * platform-supplied `ctx.access.getIdentity()` (Access policies attached to the
87
+ * Worker). Both paths produce this one shape, so nothing downstream branches on
88
+ * how the caller was authenticated.
89
+ *
90
+ * Extends the standard `JWTPayload` (`iss`/`aud`/`sub`/`exp`/`iat`/…) with the
91
+ * Access-specific fields. Which optional fields are present depends on the
92
+ * caller and the Access application config. SSO users carry `email` (and
93
+ * `groups` when the policy emits them), with `sub` as the stable user id.
94
+ * Service tokens carry `common_name` and an empty `sub`; there is no `email`.
95
+ *
96
+ * Cloudflare may add further custom claims — they pass through verbatim via the
97
+ * index signature so the claims stay a faithful view of the identity.
98
+ */
99
+ interface AccessClaims extends JWTPayload {
100
+ /** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
101
+ common_name?: string;
102
+ /** ISO-3166-1 alpha-2 country the request was authorized from, when available. */
103
+ country?: string;
104
+ /** Verified user email. Present for interactive (SSO) callers. */
105
+ email?: string;
106
+ /** Identity-provider group memberships, when the Access policy is configured to emit them. */
107
+ groups?: string[];
108
+ /** Per-session nonce Cloudflare rotates on re-authentication. */
109
+ identity_nonce?: string;
110
+ /** Display name from the identity provider. Populated on the platform-supplied identity, not on the JWT. */
111
+ name?: string;
112
+ /** Token kind, e.g. `"app"`. */
113
+ type?: string;
114
+ /** Cloudflare's per-user UUID. Populated on the platform-supplied identity, not on the JWT — so `userId` is deliberately never derived from it. */
115
+ user_uuid?: string;
116
+ }
117
+ /**
118
+ * The minimal `resolveIdentity` return contract shared with `@lunora/runtime`'s
119
+ * `WorkerOptions.resolveIdentity` (`ResolvedIdentity`). Declared structurally so
120
+ * this package takes no runtime dependency on `@lunora/runtime`; the value is
121
+ * assignable to the runtime hook.
122
+ *
123
+ * `userId` becomes `ctx.auth.userId`; every other key is forwarded (server-side,
124
+ * unforgeable) into `x-lunora-identity` and surfaced via `ctx.auth.getIdentity()`.
125
+ * `exp` (JWT epoch **seconds**) drives WebSocket credential expiry — omit it and
126
+ * a live subscription socket never expires.
127
+ */
128
+ interface ResolvedIdentityLike {
129
+ /** All other claims pass through into `ctx.auth.getIdentity()`. */
130
+ [claim: string]: unknown;
131
+ /** JWT `exp` in epoch **seconds** (NOT milliseconds). Drives WS socket expiry. */
132
+ exp?: number;
133
+ /** Absolute expiry in epoch **milliseconds**. Alternative to `exp`; takes precedence in the runtime. */
134
+ expiresAtMs?: number;
135
+ /** The stable caller id. Becomes `ctx.auth.userId` and what `serverDefault(({auth}) => auth.userId)` stamps. */
136
+ userId: string;
137
+ }
138
+ /**
139
+ * The verified Access identity produced by `createAccessResolver`. A
140
+ * {@link ResolvedIdentityLike} with the commonly-used Access claims promoted to
141
+ * named, camelCased fields (so policies read `auth.identity.groups` etc.) plus
142
+ * the full raw claim set under `access` for fidelity.
143
+ */
144
+ interface ResolvedAccessIdentity extends ResolvedIdentityLike {
145
+ /** The full, verified claim set (snake_cased wire names preserved). */
146
+ access: AccessClaims;
147
+ /** Service-token name (`common_name`), for machine callers. */
148
+ commonName?: string;
149
+ /** Verified email, for SSO callers. */
150
+ email?: string;
151
+ /** IdP group memberships, when emitted by the Access policy. */
152
+ groups?: string[];
153
+ }
154
+ /**
155
+ * A key source for `verifyAccessJwt`. Either a `jose` remote/local JWKS getter,
156
+ * or a single public key (handy for tests that mint their own RS256 tokens).
157
+ * When omitted, a cached remote JWKS is built from `teamDomain`.
158
+ */
159
+ type AccessKeySet = CryptoKey | JWTVerifyGetKey | KeyObject | Uint8Array;
160
+ /** Options for `verifyAccessJwt`. */
161
+ interface VerifyAccessJwtOptions {
162
+ /**
163
+ * The Access application **AUD tag(s)** (the application audience from the
164
+ * Access app's Overview). Verification rejects a token whose `aud` does not
165
+ * include one of these — this is what scopes a token to *your* app.
166
+ */
167
+ aud: string | string[];
168
+ /** Clock-skew tolerance in **seconds** applied to `exp`/`nbf`/`iat`. Default `0`. */
169
+ clockToleranceSec?: number;
170
+ /**
171
+ * Override the verification key source. Primarily for tests; in production
172
+ * leave unset to use the cached remote JWKS derived from `teamDomain`.
173
+ */
174
+ keySet?: AccessKeySet;
175
+ /**
176
+ * Your Cloudflare Access team domain. Accepts the short team name (`acme`),
177
+ * the host (`acme.cloudflareaccess.com`), or a full URL
178
+ * (`https://acme.cloudflareaccess.com`). Determines both the expected issuer
179
+ * and the JWKS endpoint.
180
+ */
181
+ teamDomain: string;
182
+ }
183
+ /**
184
+ * Common options for the request-driven Access primitives — how to read the JWT
185
+ * off the request and what to do when verification fails. Shared by
186
+ * {@link CreateAccessResolverOptions} and `AccessAdminGateOptions`, which add
187
+ * their distinct mapping / authorization step on top.
188
+ */
189
+ interface RequestVerifyOptions extends VerifyAccessJwtOptions {
190
+ /**
191
+ * Cookie name carrying the Access JWT when the header is absent (browser
192
+ * navigations). Default `"CF_Authorization"`.
193
+ */
194
+ cookieName?: string;
195
+ /**
196
+ * Request header carrying the Access JWT. Default `"cf-access-jwt-assertion"`
197
+ * (matched case-insensitively).
198
+ */
199
+ headerName?: string;
200
+ /**
201
+ * Invoked when a token is present but fails verification (bad signature,
202
+ * wrong audience, expired, …). The caller still fails closed (resolver
203
+ * returns `null`, admin gate returns `false`); this is your hook to
204
+ * log/observe. It is **not** called when no token is present at all.
205
+ */
206
+ onError?: (error: unknown, request: Request) => void;
207
+ }
208
+ /**
209
+ * {@link RequestVerifyOptions} with the JWT-verification config made optional,
210
+ * for the primitives that can also authenticate off the platform-supplied
211
+ * identity (`ctx.access`) and therefore may legitimately be given no JWT config
212
+ * at all.
213
+ *
214
+ * The two fields are **all-or-nothing**: supply both to enable the
215
+ * `Cf-Access-Jwt-Assertion` fallback (needed for hostname-scoped Access
216
+ * applications, which do not populate `ctx.access`), or neither to run
217
+ * platform-identity-only. Supplying exactly one throws at construction — that is
218
+ * always a misconfiguration (classically an unset `env.CF_ACCESS_AUD`), and
219
+ * silently degrading it to "no JWT fallback" would turn a broken deployment into
220
+ * a quietly anonymous one.
221
+ */
222
+ interface AccessJwtFallbackOptions extends Omit<RequestVerifyOptions, "aud" | "teamDomain"> {
223
+ /**
224
+ * The Access application **AUD tag(s)**. Required together with `teamDomain`
225
+ * to enable JWT verification; omit both to authenticate only off the
226
+ * platform-supplied identity.
227
+ */
228
+ aud?: string | string[];
229
+ /**
230
+ * Your Cloudflare Access team domain. Required together with `aud` to enable
231
+ * JWT verification; omit both to authenticate only off the platform-supplied
232
+ * identity.
233
+ */
234
+ teamDomain?: string;
235
+ }
236
+ /** Options for `createAccessResolver`; extends {@link AccessJwtFallbackOptions}. */
237
+ interface CreateAccessResolverOptions extends AccessJwtFallbackOptions {
238
+ /**
239
+ * Remap verified claims into the resolved identity. Return an object to
240
+ * shallow-merge over the defaults; return a `userId` to override the derived
241
+ * caller id. Runs only after signature/issuer/audience/expiry are verified.
242
+ */
243
+ mapClaims?: (claims: AccessClaims) => Record<string, unknown>;
244
+ }
245
+ /**
246
+ * A `resolveIdentity`-shaped function: maps an inbound request to a verified
247
+ * identity (or `null` for anonymous). Assignable to `@lunora/runtime`'s
248
+ * `WorkerOptions.resolveIdentity`.
249
+ *
250
+ * The third argument is the request's `ExecutionContext`, which the runtime
251
+ * forwards so a resolver can read the identity Cloudflare Access attaches to a
252
+ * Worker-protected request (`context.access`). It is `undefined` on paths that
253
+ * have no context to give, so a resolver must handle its absence.
254
+ */
255
+ type ResolveIdentityFunction = (request: Request, env?: unknown, context?: ExecutionContextLike) => (ResolvedIdentityLike | null) | Promise<ResolvedIdentityLike | null>;
256
+ export { AccessClaims as A, CreateAccessResolverOptions as C, ExecutionContextLike as E, ResolveIdentityFunction as R, VerifyAccessJwtOptions as V, AccessJwtFallbackOptions as a, AccessKeySet as b, ResolvedAccessIdentity as c, ResolvedIdentityLike as d };
package/dist/roles.d.mts CHANGED
@@ -1,11 +1,10 @@
1
- import { M as Middleware } from "./packem_shared/index.d-ByAnpUzP.mjs";
2
- import '@lunora/errors';
1
+ import { M as Middleware } from "./packem_shared/index.d-D7R8k7yf.mjs";
3
2
  /**
4
- * The slice of context {@link accessRoles} reads and augments: the `auth` facade
5
- * every Lunora ctx carries. `getIdentity()` returns the verified identity
6
- * envelope (`createAccessResolver`'s output, including `groups`); `roles` is the
7
- * per-request role list `rls()` unions permissions over.
8
- */
3
+ * The slice of context {@link accessRoles} reads and augments: the `auth` facade
4
+ * every Lunora ctx carries. `getIdentity()` returns the verified identity
5
+ * envelope (`createAccessResolver`'s output, including `groups`); `roles` is the
6
+ * per-request role list `rls()` unions permissions over.
7
+ */
9
8
  interface AccessRolesContext {
10
9
  auth?: {
11
10
  getIdentity?: () => (Record<string, unknown> | null) | Promise<Record<string, unknown> | null>;
@@ -18,36 +17,36 @@ type AccessRoleMap = ((group: string) => string | string[] | undefined) | Record
18
17
  /** Options for {@link accessRoles}. */
19
18
  interface AccessRolesOptions {
20
19
  /**
21
- * Map verified Access group names to RLS role names. A table
22
- * (`{ "idp-admins": "admin", "idp-eng": ["editor", "viewer"] }`) or a
23
- * function; either may return one role, an array, or `undefined` to drop the
24
- * group. Omit to use each group name verbatim as a role.
25
- */
20
+ * Map verified Access group names to RLS role names. A table
21
+ * (`{ "idp-admins": "admin", "idp-eng": ["editor", "viewer"] }`) or a
22
+ * function; either may return one role, an array, or `undefined` to drop the
23
+ * group. Omit to use each group name verbatim as a role.
24
+ */
26
25
  map?: AccessRoleMap;
27
26
  /**
28
- * Read the group list off the resolved identity. Defaults to the `groups`
29
- * claim (`string[]`). Override when your IdP nests groups elsewhere.
30
- */
27
+ * Read the group list off the resolved identity. Defaults to the `groups`
28
+ * claim (`string[]`). Override when your IdP nests groups elsewhere.
29
+ */
31
30
  readGroups?: (identity: Record<string, unknown>) => ReadonlyArray<string> | undefined;
32
31
  }
33
32
  /**
34
- * Middleware that lifts the verified Cloudflare Access `groups` claim into
35
- * `ctx.auth.roles` so `rls()` policies can authorize by role. Place it **before**
36
- * `rls(...)` in the `.use(...)` chain — `rls()` reads `ctx.auth.roles` to union
37
- * the permissions a request carries.
38
- *
39
- * It reads the resolved identity via `ctx.auth.getIdentity()` (the output of
40
- * `createAccessResolver`), maps each group to role name(s), and unions them with
41
- * any roles already on `ctx.auth.roles` (so a role set by an earlier middleware
42
- * is preserved). When there is no identity or no groups it forwards the context
43
- * unchanged — anonymous requests stay role-less (fail-closed under RLS).
44
- *
45
- * ```ts
46
- * export const listInvoices = query
47
- * .use(accessRoles({ map: { "idp-admins": "admin", "idp-billing": ["billing", "viewer"] } }))
48
- * .use(rls(policies, { roles }))
49
- * .query(async ({ ctx }) => ...);
50
- * ```
51
- */
33
+ * Middleware that lifts the verified Cloudflare Access `groups` claim into
34
+ * `ctx.auth.roles` so `rls()` policies can authorize by role. Place it **before**
35
+ * `rls(...)` in the `.use(...)` chain — `rls()` reads `ctx.auth.roles` to union
36
+ * the permissions a request carries.
37
+ *
38
+ * It reads the resolved identity via `ctx.auth.getIdentity()` (the output of
39
+ * `createAccessResolver`), maps each group to role name(s), and unions them with
40
+ * any roles already on `ctx.auth.roles` (so a role set by an earlier middleware
41
+ * is preserved). When there is no identity or no groups it forwards the context
42
+ * unchanged — anonymous requests stay role-less (fail-closed under RLS).
43
+ *
44
+ * ```ts
45
+ * export const listInvoices = query
46
+ * .use(accessRoles({ map: { "idp-admins": "admin", "idp-billing": ["billing", "viewer"] } }))
47
+ * .use(rls(policies, { roles }))
48
+ * .query(async ({ ctx }) => ...);
49
+ * ```
50
+ */
52
51
  declare const accessRoles: <Context extends AccessRolesContext>(options?: AccessRolesOptions) => Middleware<Context, Context>;
53
52
  export { type AccessRoleMap, type AccessRolesContext, type AccessRolesOptions, accessRoles };