@lunora/cloudflare-access 1.0.0-alpha.70 → 1.0.0-alpha.72
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/dist/admin.d.mts +39 -12
- package/dist/admin.d.ts +39 -12
- package/dist/admin.mjs +1 -1
- package/dist/context.d.mts +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/index.d.mts +30 -12
- package/dist/index.d.ts +30 -12
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/accessIssuer-CLO6p5MF.mjs +1 -0
- package/dist/packem_shared/composeResolvers-JBsWQvv4.mjs +1 -0
- package/dist/packem_shared/platform-identity-Dy6G1EoF.mjs +1 -0
- package/dist/packem_shared/types.d-D30o96f7.d.mts +256 -0
- package/dist/packem_shared/types.d-D30o96f7.d.ts +256 -0
- package/package.json +2 -2
- package/dist/packem_shared/accessIssuer-B6vVJPus.mjs +0 -1
- package/dist/packem_shared/composeResolvers--vZvblPO.mjs +0 -1
- package/dist/packem_shared/types.d-C8c7Qwx1.d.mts +0 -134
- package/dist/packem_shared/types.d-C8c7Qwx1.d.ts +0 -134
package/dist/admin.d.mts
CHANGED
|
@@ -1,36 +1,63 @@
|
|
|
1
|
-
import { A as AccessClaims,
|
|
1
|
+
import { A as AccessClaims, a as AccessJwtFallbackOptions, E as ExecutionContextLike } from "./packem_shared/types.d-D30o96f7.mjs";
|
|
2
2
|
import 'jose';
|
|
3
|
-
/** Options for {@link accessAdminGate}; extends {@link
|
|
4
|
-
interface AccessAdminGateOptions extends
|
|
3
|
+
/** Options for {@link accessAdminGate}; extends {@link AccessJwtFallbackOptions}. */
|
|
4
|
+
interface AccessAdminGateOptions extends AccessJwtFallbackOptions {
|
|
5
5
|
/**
|
|
6
6
|
* Decide whether the **verified** claims authorize the Studio/admin plane —
|
|
7
7
|
* e.g. `(claims) => claims.groups?.includes("ops") ?? false` or an email-domain
|
|
8
8
|
* check. Required: there is no implicit grant, so a verified-but-unprivileged
|
|
9
|
-
* identity is denied. Runs only after
|
|
9
|
+
* identity is denied. Runs only after the caller is authenticated.
|
|
10
10
|
*/
|
|
11
11
|
isAdmin: (claims: AccessClaims) => boolean | Promise<boolean>;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* Build an admin gate for `@lunora/runtime`'s `WorkerOptions.adminGate`: a
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* predicate that authenticates the caller through Cloudflare Access and applies
|
|
16
|
+
* your `isAdmin(claims)` test. When it resolves `true` the request authorizes the
|
|
17
|
+
* `/_lunora/admin/*` plane (the Studio's HTTP + WS endpoints) in addition to — or
|
|
18
|
+
* instead of — the static admin bearer, so the Studio can sit behind Cloudflare
|
|
19
|
+
* Access.
|
|
20
20
|
*
|
|
21
|
-
* It
|
|
21
|
+
* It accepts exactly one proof, and **which one is your choice, not the
|
|
22
|
+
* platform's**:
|
|
23
|
+
*
|
|
24
|
+
* - Configure `teamDomain` + `aud` and the request's `Cf-Access-Jwt-Assertion`
|
|
25
|
+
* JWT is the only thing accepted. This is the stricter setup, and the point of
|
|
26
|
+
* the `aud`: it proves the caller came through the *specific* Access application
|
|
27
|
+
* you put in front of `/_lunora/admin`, not merely through some application in
|
|
28
|
+
* the same Cloudflare team.
|
|
29
|
+
* - Configure neither and the Worker's own Access identity (`context.access`) is
|
|
30
|
+
* accepted, for a deployment whose Access policy is attached to the Worker.
|
|
31
|
+
*
|
|
32
|
+
* Note this is the **opposite** precedence to `createAccessResolver`, which
|
|
33
|
+
* prefers the platform identity. That is deliberate. A policy attached to the
|
|
34
|
+
* Worker is typically broad — "anyone at the company", covering every route and
|
|
35
|
+
* preview URL — while a configured admin `aud` is deliberately narrow. Letting
|
|
36
|
+
* the broad one satisfy a gate you scoped with the narrow one would widen the
|
|
37
|
+
* admin plane to everyone the Worker policy admits, silently, the moment that
|
|
38
|
+
* policy was attached.
|
|
39
|
+
*
|
|
40
|
+
* It is **fail-closed**: no identity, a token that fails verification, or an
|
|
22
41
|
* `isAdmin` that returns `false` all resolve to `false` (the bearer remains the
|
|
23
42
|
* only other path). Verification needs no `env` binding (static team-domain/aud
|
|
24
43
|
* config + the remote JWKS over `fetch`), so the gate takes only the request and
|
|
25
|
-
* the runtime can evaluate it without threading async through
|
|
44
|
+
* its context and the runtime can evaluate it without threading async through
|
|
45
|
+
* every admin route.
|
|
26
46
|
*
|
|
27
47
|
* ```ts
|
|
48
|
+
* // A dedicated Access application over /_lunora/admin — the JWT is the only proof.
|
|
28
49
|
* options.adminGate = accessAdminGate({
|
|
29
50
|
* teamDomain: env.CF_ACCESS_TEAM_DOMAIN,
|
|
30
51
|
* aud: env.CF_ACCESS_ADMIN_AUD,
|
|
31
52
|
* isAdmin: (claims) => claims.groups?.includes("lunora-admins") ?? false,
|
|
32
53
|
* });
|
|
54
|
+
*
|
|
55
|
+
* // Access policy attached to the Worker — `isAdmin` is the whole boundary, so
|
|
56
|
+
* // make it at least as strict as a dedicated admin application would have been.
|
|
57
|
+
* options.adminGate = accessAdminGate({
|
|
58
|
+
* isAdmin: (claims) => claims.groups?.includes("lunora-admins") ?? false,
|
|
59
|
+
* });
|
|
33
60
|
* ```
|
|
34
61
|
*/
|
|
35
|
-
declare const accessAdminGate: (options: AccessAdminGateOptions) => ((request: Request) => Promise<boolean>);
|
|
62
|
+
declare const accessAdminGate: (options: AccessAdminGateOptions) => ((request: Request, context?: ExecutionContextLike) => Promise<boolean>);
|
|
36
63
|
export { type AccessAdminGateOptions, accessAdminGate };
|
package/dist/admin.d.ts
CHANGED
|
@@ -1,36 +1,63 @@
|
|
|
1
|
-
import { A as AccessClaims,
|
|
1
|
+
import { A as AccessClaims, a as AccessJwtFallbackOptions, E as ExecutionContextLike } from "./packem_shared/types.d-D30o96f7.js";
|
|
2
2
|
import 'jose';
|
|
3
|
-
/** Options for {@link accessAdminGate}; extends {@link
|
|
4
|
-
interface AccessAdminGateOptions extends
|
|
3
|
+
/** Options for {@link accessAdminGate}; extends {@link AccessJwtFallbackOptions}. */
|
|
4
|
+
interface AccessAdminGateOptions extends AccessJwtFallbackOptions {
|
|
5
5
|
/**
|
|
6
6
|
* Decide whether the **verified** claims authorize the Studio/admin plane —
|
|
7
7
|
* e.g. `(claims) => claims.groups?.includes("ops") ?? false` or an email-domain
|
|
8
8
|
* check. Required: there is no implicit grant, so a verified-but-unprivileged
|
|
9
|
-
* identity is denied. Runs only after
|
|
9
|
+
* identity is denied. Runs only after the caller is authenticated.
|
|
10
10
|
*/
|
|
11
11
|
isAdmin: (claims: AccessClaims) => boolean | Promise<boolean>;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* Build an admin gate for `@lunora/runtime`'s `WorkerOptions.adminGate`: a
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* predicate that authenticates the caller through Cloudflare Access and applies
|
|
16
|
+
* your `isAdmin(claims)` test. When it resolves `true` the request authorizes the
|
|
17
|
+
* `/_lunora/admin/*` plane (the Studio's HTTP + WS endpoints) in addition to — or
|
|
18
|
+
* instead of — the static admin bearer, so the Studio can sit behind Cloudflare
|
|
19
|
+
* Access.
|
|
20
20
|
*
|
|
21
|
-
* It
|
|
21
|
+
* It accepts exactly one proof, and **which one is your choice, not the
|
|
22
|
+
* platform's**:
|
|
23
|
+
*
|
|
24
|
+
* - Configure `teamDomain` + `aud` and the request's `Cf-Access-Jwt-Assertion`
|
|
25
|
+
* JWT is the only thing accepted. This is the stricter setup, and the point of
|
|
26
|
+
* the `aud`: it proves the caller came through the *specific* Access application
|
|
27
|
+
* you put in front of `/_lunora/admin`, not merely through some application in
|
|
28
|
+
* the same Cloudflare team.
|
|
29
|
+
* - Configure neither and the Worker's own Access identity (`context.access`) is
|
|
30
|
+
* accepted, for a deployment whose Access policy is attached to the Worker.
|
|
31
|
+
*
|
|
32
|
+
* Note this is the **opposite** precedence to `createAccessResolver`, which
|
|
33
|
+
* prefers the platform identity. That is deliberate. A policy attached to the
|
|
34
|
+
* Worker is typically broad — "anyone at the company", covering every route and
|
|
35
|
+
* preview URL — while a configured admin `aud` is deliberately narrow. Letting
|
|
36
|
+
* the broad one satisfy a gate you scoped with the narrow one would widen the
|
|
37
|
+
* admin plane to everyone the Worker policy admits, silently, the moment that
|
|
38
|
+
* policy was attached.
|
|
39
|
+
*
|
|
40
|
+
* It is **fail-closed**: no identity, a token that fails verification, or an
|
|
22
41
|
* `isAdmin` that returns `false` all resolve to `false` (the bearer remains the
|
|
23
42
|
* only other path). Verification needs no `env` binding (static team-domain/aud
|
|
24
43
|
* config + the remote JWKS over `fetch`), so the gate takes only the request and
|
|
25
|
-
* the runtime can evaluate it without threading async through
|
|
44
|
+
* its context and the runtime can evaluate it without threading async through
|
|
45
|
+
* every admin route.
|
|
26
46
|
*
|
|
27
47
|
* ```ts
|
|
48
|
+
* // A dedicated Access application over /_lunora/admin — the JWT is the only proof.
|
|
28
49
|
* options.adminGate = accessAdminGate({
|
|
29
50
|
* teamDomain: env.CF_ACCESS_TEAM_DOMAIN,
|
|
30
51
|
* aud: env.CF_ACCESS_ADMIN_AUD,
|
|
31
52
|
* isAdmin: (claims) => claims.groups?.includes("lunora-admins") ?? false,
|
|
32
53
|
* });
|
|
54
|
+
*
|
|
55
|
+
* // Access policy attached to the Worker — `isAdmin` is the whole boundary, so
|
|
56
|
+
* // make it at least as strict as a dedicated admin application would have been.
|
|
57
|
+
* options.adminGate = accessAdminGate({
|
|
58
|
+
* isAdmin: (claims) => claims.groups?.includes("lunora-admins") ?? false,
|
|
59
|
+
* });
|
|
33
60
|
* ```
|
|
34
61
|
*/
|
|
35
|
-
declare const accessAdminGate: (options: AccessAdminGateOptions) => ((request: Request) => Promise<boolean>);
|
|
62
|
+
declare const accessAdminGate: (options: AccessAdminGateOptions) => ((request: Request, context?: ExecutionContextLike) => Promise<boolean>);
|
|
36
63
|
export { type AccessAdminGateOptions, accessAdminGate };
|
package/dist/admin.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{r as e}from"./packem_shared/platform-identity-Dy6G1EoF.mjs";import{assertJwtFallbackOptions as o,verifyRequest as n}from"./packem_shared/accessIssuer-CLO6p5MF.mjs";const d=t=>{const a=o(t);return async(s,i)=>{const r=a===void 0?await e(i):await n(s,a);return r===void 0?!1:t.isAdmin(r)}};export{d as accessAdminGate};
|
package/dist/context.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as Middleware } from "./packem_shared/index.d-D7R8k7yf.mjs";
|
|
2
|
-
import { A as AccessClaims } from "./packem_shared/types.d-
|
|
2
|
+
import { A as AccessClaims } from "./packem_shared/types.d-D30o96f7.mjs";
|
|
3
3
|
import 'jose';
|
|
4
4
|
/**
|
|
5
5
|
* The slice of context {@link accessContext} reads: the `auth` facade every
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as Middleware } from "./packem_shared/index.d-D7R8k7yf.js";
|
|
2
|
-
import { A as AccessClaims } from "./packem_shared/types.d-
|
|
2
|
+
import { A as AccessClaims } from "./packem_shared/types.d-D30o96f7.js";
|
|
3
3
|
import 'jose';
|
|
4
4
|
/**
|
|
5
5
|
* The slice of context {@link accessContext} reads: the `auth` facade every
|
package/dist/index.d.mts
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type { b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-
|
|
1
|
+
import { R as ResolveIdentityFunction, C as CreateAccessResolverOptions, V as VerifyAccessJwtOptions, A as AccessClaims } from "./packem_shared/types.d-D30o96f7.mjs";
|
|
2
|
+
export type { a as AccessJwtFallbackOptions, b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-D30o96f7.mjs";
|
|
3
3
|
import 'jose';
|
|
4
4
|
/**
|
|
5
|
-
* Create a `resolveIdentity` adapter for Cloudflare Access
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* verified Access user/service-token becomes `ctx.auth` for every
|
|
9
|
-
* query/mutation/action (and feeds RLS) with no further wiring.
|
|
5
|
+
* Create a `resolveIdentity` adapter for Cloudflare Access, so a verified Access
|
|
6
|
+
* user or service token becomes `ctx.auth` for every query/mutation/action (and
|
|
7
|
+
* feeds RLS) with no further wiring.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* It authenticates from whichever of the two Access shapes the deployment uses,
|
|
10
|
+
* **platform identity first**:
|
|
11
|
+
*
|
|
12
|
+
* 1. `context.access` — the identity Cloudflare attaches when the Access policy
|
|
13
|
+
* is attached to the **Worker** (covering its custom domains, routes,
|
|
14
|
+
* `workers.dev`, and preview URLs at once). Nothing is verified because nothing
|
|
15
|
+
* can be forged: the platform authenticated the caller before the Worker ran,
|
|
16
|
+
* and the field is absent unless it did. No JWKS fetch, no `aud` to get wrong.
|
|
17
|
+
* This is also what `wrangler.jsonc`'s `access.dev` block simulates, so a
|
|
18
|
+
* locally-simulated identity reaches `ctx.auth` too.
|
|
19
|
+
* 2. The `Cf-Access-Jwt-Assertion` header (or `CF_Authorization` cookie),
|
|
20
|
+
* verified against your team JWKS. Needed for **hostname-scoped** Access
|
|
21
|
+
* applications, which do not populate `context.access`. Configured by passing
|
|
22
|
+
* `teamDomain` + `aud`; omit both (or pass no options at all) to run
|
|
23
|
+
* platform-identity-only.
|
|
24
|
+
*
|
|
25
|
+
* Behaviour is **fail-closed → anonymous** on both paths: no identity, or a token
|
|
26
|
+
* that fails verification, resolves to `null` (the request proceeds
|
|
27
|
+
* unauthenticated and RLS denies). Use {@link CreateAccessResolverOptions.onError}
|
|
28
|
+
* to observe verification failures.
|
|
15
29
|
*
|
|
16
30
|
* Wire it in your worker entry:
|
|
17
31
|
*
|
|
18
32
|
* ```ts
|
|
33
|
+
* // Access policy attached to the Worker — nothing to configure.
|
|
34
|
+
* options.resolveIdentity = createAccessResolver();
|
|
35
|
+
*
|
|
36
|
+
* // Hostname-scoped Access application — JWT verification config required.
|
|
19
37
|
* options.resolveIdentity = createAccessResolver({
|
|
20
38
|
* teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
|
|
21
39
|
* aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
|
|
22
40
|
* });
|
|
23
41
|
* ```
|
|
24
42
|
*/
|
|
25
|
-
declare const createAccessResolver: (options
|
|
43
|
+
declare const createAccessResolver: (options?: CreateAccessResolverOptions) => ResolveIdentityFunction;
|
|
26
44
|
/**
|
|
27
45
|
* Compose several `resolveIdentity` adapters into one: each is tried in order
|
|
28
46
|
* and the first to return a non-null identity wins. The canonical use is
|
package/dist/index.d.ts
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type { b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-
|
|
1
|
+
import { R as ResolveIdentityFunction, C as CreateAccessResolverOptions, V as VerifyAccessJwtOptions, A as AccessClaims } from "./packem_shared/types.d-D30o96f7.js";
|
|
2
|
+
export type { a as AccessJwtFallbackOptions, b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-D30o96f7.js";
|
|
3
3
|
import 'jose';
|
|
4
4
|
/**
|
|
5
|
-
* Create a `resolveIdentity` adapter for Cloudflare Access
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* verified Access user/service-token becomes `ctx.auth` for every
|
|
9
|
-
* query/mutation/action (and feeds RLS) with no further wiring.
|
|
5
|
+
* Create a `resolveIdentity` adapter for Cloudflare Access, so a verified Access
|
|
6
|
+
* user or service token becomes `ctx.auth` for every query/mutation/action (and
|
|
7
|
+
* feeds RLS) with no further wiring.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* It authenticates from whichever of the two Access shapes the deployment uses,
|
|
10
|
+
* **platform identity first**:
|
|
11
|
+
*
|
|
12
|
+
* 1. `context.access` — the identity Cloudflare attaches when the Access policy
|
|
13
|
+
* is attached to the **Worker** (covering its custom domains, routes,
|
|
14
|
+
* `workers.dev`, and preview URLs at once). Nothing is verified because nothing
|
|
15
|
+
* can be forged: the platform authenticated the caller before the Worker ran,
|
|
16
|
+
* and the field is absent unless it did. No JWKS fetch, no `aud` to get wrong.
|
|
17
|
+
* This is also what `wrangler.jsonc`'s `access.dev` block simulates, so a
|
|
18
|
+
* locally-simulated identity reaches `ctx.auth` too.
|
|
19
|
+
* 2. The `Cf-Access-Jwt-Assertion` header (or `CF_Authorization` cookie),
|
|
20
|
+
* verified against your team JWKS. Needed for **hostname-scoped** Access
|
|
21
|
+
* applications, which do not populate `context.access`. Configured by passing
|
|
22
|
+
* `teamDomain` + `aud`; omit both (or pass no options at all) to run
|
|
23
|
+
* platform-identity-only.
|
|
24
|
+
*
|
|
25
|
+
* Behaviour is **fail-closed → anonymous** on both paths: no identity, or a token
|
|
26
|
+
* that fails verification, resolves to `null` (the request proceeds
|
|
27
|
+
* unauthenticated and RLS denies). Use {@link CreateAccessResolverOptions.onError}
|
|
28
|
+
* to observe verification failures.
|
|
15
29
|
*
|
|
16
30
|
* Wire it in your worker entry:
|
|
17
31
|
*
|
|
18
32
|
* ```ts
|
|
33
|
+
* // Access policy attached to the Worker — nothing to configure.
|
|
34
|
+
* options.resolveIdentity = createAccessResolver();
|
|
35
|
+
*
|
|
36
|
+
* // Hostname-scoped Access application — JWT verification config required.
|
|
19
37
|
* options.resolveIdentity = createAccessResolver({
|
|
20
38
|
* teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
|
|
21
39
|
* aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
|
|
22
40
|
* });
|
|
23
41
|
* ```
|
|
24
42
|
*/
|
|
25
|
-
declare const createAccessResolver: (options
|
|
43
|
+
declare const createAccessResolver: (options?: CreateAccessResolverOptions) => ResolveIdentityFunction;
|
|
26
44
|
/**
|
|
27
45
|
* Compose several `resolveIdentity` adapters into one: each is tried in order
|
|
28
46
|
* and the first to return a non-null identity wins. The canonical use is
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{composeResolvers as r,createAccessResolver as c}from"./packem_shared/composeResolvers
|
|
1
|
+
import{composeResolvers as r,createAccessResolver as c}from"./packem_shared/composeResolvers-JBsWQvv4.mjs";import{accessIssuer as t,verifyAccessJwt as f}from"./packem_shared/accessIssuer-CLO6p5MF.mjs";export{t as accessIssuer,r as composeResolvers,c as createAccessResolver,f as verifyAccessJwt};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as a}from"@lunora/errors";import{jwtVerify as m,createRemoteJWKSet as f}from"jose";const h="cf-access-jwt-assertion",w="CF_Authorization",y=(t,e,r)=>{const o=t.headers.get(e);if(o!==null&&o.length>0)return o;const n=t.headers.get("cookie");if(n!==null)for(const c of n.split(";")){const s=c.indexOf("=");if(s!==-1&&c.slice(0,s).trim()===r){const i=c.slice(s+1).trim();return i.length>0?i:void 0}}},A="/cdn-cgi/access/certs",g=/^https?:\/\//i,v=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},d=t=>{const e=v(t.trim().replace(g,""));if(e.length===0)throw new a("INTERNAL",'@lunora/cloudflare-access: `teamDomain` is required (e.g. "acme" or "acme.cloudflareaccess.com")');const r=e.includes(".")?`https://${e}`:`https://${e}.cloudflareaccess.com`;return`https://${new URL(r).host.toLowerCase()}`},l=new Map,E=t=>{let e=l.get(t);return e===void 0&&(e=f(new URL(`${t}${A}`)),l.set(t,e)),e},u=t=>{const e=(Array.isArray(t)?t:[t]).filter(r=>typeof r=="string"&&r.length>0);if(e.length===0)throw new a("INTERNAL","@lunora/cloudflare-access: `aud` is required and must be a non-empty Access AUD tag — refusing to verify a token without an audience to scope it to your application");return e},k=t=>{d(t.teamDomain),u(t.aud)},S=t=>{if(t===void 0||!("aud"in t)&&!("teamDomain"in t))return;const{aud:e,teamDomain:r}=t;if(e===void 0||r===void 0)throw new a("INTERNAL","@lunora/cloudflare-access: `teamDomain` and `aud` must both be set to verify the Cf-Access-Jwt-Assertion JWT (check that CF_ACCESS_TEAM_DOMAIN / CF_ACCESS_AUD are set in this environment) — to authenticate only off the Worker's Cloudflare Access identity, omit both options entirely");const o={...t,aud:e,teamDomain:r};return k(o),o},C=async(t,e)=>{const r=d(e.teamDomain),o=u(e.aud),n=e.keySet??E(r),{payload:c}=await m(t,n,{algorithms:["RS256"],audience:o,clockTolerance:e.clockToleranceSec,issuer:r});return c},p=async(t,e)=>{const r=(e.headerName??h).toLowerCase(),o=e.cookieName??w,n=y(t,r,o);if(n!==void 0)try{return await C(n,e)}catch(c){try{e.onError?.(c,t)}catch{}return}};export{d as accessIssuer,S as assertJwtFallbackOptions,k as assertVerifyOptions,C as verifyAccessJwt,p as verifyRequest};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as c}from"./platform-identity-Dy6G1EoF.mjs";import{assertJwtFallbackOptions as m,verifyRequest as p}from"./accessIssuer-CLO6p5MF.mjs";const d=null,s=o=>typeof o=="string"&&o.length>0?o:void 0,v=o=>s(o.sub)??s(o.email)??s(o.common_name),a=(o,e)=>{const t=e?.(o)??{},n=s(t.userId)??v(o);return n===void 0?d:{access:o,...o.common_name===void 0?{}:{commonName:o.common_name},...o.email===void 0?{}:{email:o.email},...o.exp===void 0?{}:{exp:o.exp},...o.groups===void 0?{}:{groups:o.groups},...t,userId:n}},y=o=>{const e=m(o);return async(t,n,i)=>{const r=await c(i)??(e===void 0?void 0:await p(t,e));return r===void 0?d:a(r,o?.mapClaims)}},g=(...o)=>async(e,t,n)=>{for(const i of o){const r=await i(e,t,n);if(r)return r}return d};export{g as composeResolvers,y as createAccessResolver};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const s=t=>{if(typeof t=="string")return t.length>0?t:void 0;if(typeof t=="object"&&t!==null){const{name:o}=t;return typeof o=="string"&&o.length>0?o:void 0}},a=t=>Array.isArray(t)?t.map(o=>s(o)).filter(o=>o!==void 0):void 0,c=async t=>{const o=t?.access;if(o===void 0)return;let r;try{r=await o.getIdentity()}catch{return}if(r===null||typeof r!="object")return;const{groups:n,...e}=r,i=a(n);return i===void 0?e:{...e,groups:i}};export{c as r};
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/cloudflare-access",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.72",
|
|
4
4
|
"description": "Cloudflare Access (Zero Trust) identity for Lunora — verify the Cf-Access-Jwt-Assertion JWT against your team JWKS and feed the verified identity into ctx.auth / RLS via a resolveIdentity adapter",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"access",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
57
|
+
"@lunora/errors": "1.0.0-alpha.22",
|
|
58
58
|
"jose": "^6.2.8"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as l}from"@lunora/errors";import{jwtVerify as m,createRemoteJWKSet as h}from"jose";const f="cf-access-jwt-assertion",w="CF_Authorization",g=(t,e,r)=>{const c=t.headers.get(e);if(c!==null&&c.length>0)return c;const o=t.headers.get("cookie");if(o!==null)for(const n of o.split(";")){const s=n.indexOf("=");if(s!==-1&&n.slice(0,s).trim()===r){const a=n.slice(s+1).trim();return a.length>0?a:void 0}}},y="/cdn-cgi/access/certs",k=/^https?:\/\//i,A=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)},u=t=>{const e=A(t.trim().replace(k,""));if(e.length===0)throw new l("INTERNAL",'@lunora/cloudflare-access: `teamDomain` is required (e.g. "acme" or "acme.cloudflareaccess.com")');const r=e.includes(".")?`https://${e}`:`https://${e}.cloudflareaccess.com`;return`https://${new URL(r).host.toLowerCase()}`},i=new Map,E=t=>{let e=i.get(t);return e===void 0&&(e=h(new URL(`${t}${y}`)),i.set(t,e)),e},d=t=>{const e=(Array.isArray(t)?t:[t]).filter(r=>typeof r=="string"&&r.length>0);if(e.length===0)throw new l("INTERNAL","@lunora/cloudflare-access: `aud` is required and must be a non-empty Access AUD tag — refusing to verify a token without an audience to scope it to your application");return e},v=t=>{u(t.teamDomain),d(t.aud)},p=async(t,e)=>{const r=u(e.teamDomain),c=d(e.aud),o=e.keySet??E(r),{payload:n}=await m(t,o,{algorithms:["RS256"],audience:c,clockTolerance:e.clockToleranceSec,issuer:r});return n},L=async(t,e)=>{const r=(e.headerName??f).toLowerCase(),c=e.cookieName??w,o=g(t,r,c);if(o!==void 0)try{return await p(o,e)}catch(n){try{e.onError?.(n,t)}catch{}return}};export{u as accessIssuer,v as assertVerifyOptions,p as verifyAccessJwt,L as verifyRequest};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{assertVerifyOptions as i,verifyRequest as c}from"./accessIssuer-B6vVJPus.mjs";const s=null,t=e=>typeof e=="string"&&e.length>0?e:void 0,u=e=>t(e.sub)??t(e.email)??t(e.common_name),m=(e,r)=>{const o=r?.(e)??{},n=t(o.userId)??u(e);return n===void 0?s:{access:e,...e.common_name===void 0?{}:{commonName:e.common_name},...e.email===void 0?{}:{email:e.email},...e.exp===void 0?{}:{exp:e.exp},...e.groups===void 0?{}:{groups:e.groups},...o,userId:n}},v=e=>(i(e),async r=>{const o=await c(r,e);return o===void 0?s:m(o,e.mapClaims)}),f=(...e)=>async(r,o)=>{for(const n of e){const d=await n(r,o);if(d)return d}return s};export{f as composeResolvers,v as createAccessResolver};
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { JWTPayload, JWTVerifyGetKey, KeyObject } from 'jose';
|
|
2
|
-
/**
|
|
3
|
-
* The claims Cloudflare Access mints into the `Cf-Access-Jwt-Assertion` JWT.
|
|
4
|
-
*
|
|
5
|
-
* Extends the standard `JWTPayload` (`iss`/`aud`/`sub`/`exp`/`iat`/…) with the
|
|
6
|
-
* Access-specific fields. Which optional fields are present depends on the
|
|
7
|
-
* caller and the Access application config. SSO users carry `email` (and
|
|
8
|
-
* `groups` when the policy emits them), with `sub` as the stable user id.
|
|
9
|
-
* Service tokens carry `common_name` and an empty `sub`; there is no `email`.
|
|
10
|
-
*
|
|
11
|
-
* Cloudflare may add further custom claims — they pass through verbatim via the
|
|
12
|
-
* index signature so the claims stay a faithful view of the token.
|
|
13
|
-
*/
|
|
14
|
-
interface AccessClaims extends JWTPayload {
|
|
15
|
-
/** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
|
|
16
|
-
common_name?: string;
|
|
17
|
-
/** ISO-3166-1 alpha-2 country the request was authorized from, when available. */
|
|
18
|
-
country?: string;
|
|
19
|
-
/** Verified user email. Present for interactive (SSO) callers. */
|
|
20
|
-
email?: string;
|
|
21
|
-
/** Identity-provider group memberships, when the Access policy is configured to emit them. */
|
|
22
|
-
groups?: string[];
|
|
23
|
-
/** Per-session nonce Cloudflare rotates on re-authentication. */
|
|
24
|
-
identity_nonce?: string;
|
|
25
|
-
/** Token kind, e.g. `"app"`. */
|
|
26
|
-
type?: string;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* The minimal `resolveIdentity` return contract shared with `@lunora/runtime`'s
|
|
30
|
-
* `WorkerOptions.resolveIdentity` (`ResolvedIdentity`). Declared structurally so
|
|
31
|
-
* this package takes no runtime dependency on `@lunora/runtime`; the value is
|
|
32
|
-
* assignable to the runtime hook.
|
|
33
|
-
*
|
|
34
|
-
* `userId` becomes `ctx.auth.userId`; every other key is forwarded (server-side,
|
|
35
|
-
* unforgeable) into `x-lunora-identity` and surfaced via `ctx.auth.getIdentity()`.
|
|
36
|
-
* `exp` (JWT epoch **seconds**) drives WebSocket credential expiry — omit it and
|
|
37
|
-
* a live subscription socket never expires.
|
|
38
|
-
*/
|
|
39
|
-
interface ResolvedIdentityLike {
|
|
40
|
-
/** All other claims pass through into `ctx.auth.getIdentity()`. */
|
|
41
|
-
[claim: string]: unknown;
|
|
42
|
-
/** JWT `exp` in epoch **seconds** (NOT milliseconds). Drives WS socket expiry. */
|
|
43
|
-
exp?: number;
|
|
44
|
-
/** Absolute expiry in epoch **milliseconds**. Alternative to `exp`; takes precedence in the runtime. */
|
|
45
|
-
expiresAtMs?: number;
|
|
46
|
-
/** The stable caller id. Becomes `ctx.auth.userId` and what `serverDefault(({auth}) => auth.userId)` stamps. */
|
|
47
|
-
userId: string;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* The verified Access identity produced by `createAccessResolver`. A
|
|
51
|
-
* {@link ResolvedIdentityLike} with the commonly-used Access claims promoted to
|
|
52
|
-
* named, camelCased fields (so policies read `auth.identity.groups` etc.) plus
|
|
53
|
-
* the full raw claim set under `access` for fidelity.
|
|
54
|
-
*/
|
|
55
|
-
interface ResolvedAccessIdentity extends ResolvedIdentityLike {
|
|
56
|
-
/** The full, verified claim set (snake_cased wire names preserved). */
|
|
57
|
-
access: AccessClaims;
|
|
58
|
-
/** Service-token name (`common_name`), for machine callers. */
|
|
59
|
-
commonName?: string;
|
|
60
|
-
/** Verified email, for SSO callers. */
|
|
61
|
-
email?: string;
|
|
62
|
-
/** IdP group memberships, when emitted by the Access policy. */
|
|
63
|
-
groups?: string[];
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* A key source for `verifyAccessJwt`. Either a `jose` remote/local JWKS getter,
|
|
67
|
-
* or a single public key (handy for tests that mint their own RS256 tokens).
|
|
68
|
-
* When omitted, a cached remote JWKS is built from `teamDomain`.
|
|
69
|
-
*/
|
|
70
|
-
type AccessKeySet = CryptoKey | JWTVerifyGetKey | KeyObject | Uint8Array;
|
|
71
|
-
/** Options for `verifyAccessJwt`. */
|
|
72
|
-
interface VerifyAccessJwtOptions {
|
|
73
|
-
/**
|
|
74
|
-
* The Access application **AUD tag(s)** (the application audience from the
|
|
75
|
-
* Access app's Overview). Verification rejects a token whose `aud` does not
|
|
76
|
-
* include one of these — this is what scopes a token to *your* app.
|
|
77
|
-
*/
|
|
78
|
-
aud: string | string[];
|
|
79
|
-
/** Clock-skew tolerance in **seconds** applied to `exp`/`nbf`/`iat`. Default `0`. */
|
|
80
|
-
clockToleranceSec?: number;
|
|
81
|
-
/**
|
|
82
|
-
* Override the verification key source. Primarily for tests; in production
|
|
83
|
-
* leave unset to use the cached remote JWKS derived from `teamDomain`.
|
|
84
|
-
*/
|
|
85
|
-
keySet?: AccessKeySet;
|
|
86
|
-
/**
|
|
87
|
-
* Your Cloudflare Access team domain. Accepts the short team name (`acme`),
|
|
88
|
-
* the host (`acme.cloudflareaccess.com`), or a full URL
|
|
89
|
-
* (`https://acme.cloudflareaccess.com`). Determines both the expected issuer
|
|
90
|
-
* and the JWKS endpoint.
|
|
91
|
-
*/
|
|
92
|
-
teamDomain: string;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Common options for the request-driven Access primitives — how to read the JWT
|
|
96
|
-
* off the request and what to do when verification fails. Shared by
|
|
97
|
-
* {@link CreateAccessResolverOptions} and `AccessAdminGateOptions`, which add
|
|
98
|
-
* their distinct mapping / authorization step on top.
|
|
99
|
-
*/
|
|
100
|
-
interface RequestVerifyOptions extends VerifyAccessJwtOptions {
|
|
101
|
-
/**
|
|
102
|
-
* Cookie name carrying the Access JWT when the header is absent (browser
|
|
103
|
-
* navigations). Default `"CF_Authorization"`.
|
|
104
|
-
*/
|
|
105
|
-
cookieName?: string;
|
|
106
|
-
/**
|
|
107
|
-
* Request header carrying the Access JWT. Default `"cf-access-jwt-assertion"`
|
|
108
|
-
* (matched case-insensitively).
|
|
109
|
-
*/
|
|
110
|
-
headerName?: string;
|
|
111
|
-
/**
|
|
112
|
-
* Invoked when a token is present but fails verification (bad signature,
|
|
113
|
-
* wrong audience, expired, …). The caller still fails closed (resolver
|
|
114
|
-
* returns `null`, admin gate returns `false`); this is your hook to
|
|
115
|
-
* log/observe. It is **not** called when no token is present at all.
|
|
116
|
-
*/
|
|
117
|
-
onError?: (error: unknown, request: Request) => void;
|
|
118
|
-
}
|
|
119
|
-
/** Options for `createAccessResolver`; extends {@link RequestVerifyOptions}. */
|
|
120
|
-
interface CreateAccessResolverOptions extends RequestVerifyOptions {
|
|
121
|
-
/**
|
|
122
|
-
* Remap verified claims into the resolved identity. Return an object to
|
|
123
|
-
* shallow-merge over the defaults; return a `userId` to override the derived
|
|
124
|
-
* caller id. Runs only after signature/issuer/audience/expiry are verified.
|
|
125
|
-
*/
|
|
126
|
-
mapClaims?: (claims: AccessClaims) => Record<string, unknown>;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* A `resolveIdentity`-shaped function: maps an inbound request to a verified
|
|
130
|
-
* identity (or `null` for anonymous). Assignable to `@lunora/runtime`'s
|
|
131
|
-
* `WorkerOptions.resolveIdentity`.
|
|
132
|
-
*/
|
|
133
|
-
type ResolveIdentityFunction = (request: Request, env?: unknown) => (ResolvedIdentityLike | null) | Promise<ResolvedIdentityLike | null>;
|
|
134
|
-
export { AccessClaims as A, CreateAccessResolverOptions as C, RequestVerifyOptions as R, VerifyAccessJwtOptions as V, ResolveIdentityFunction as a, AccessKeySet as b, ResolvedAccessIdentity as c, ResolvedIdentityLike as d };
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { JWTPayload, JWTVerifyGetKey, KeyObject } from 'jose';
|
|
2
|
-
/**
|
|
3
|
-
* The claims Cloudflare Access mints into the `Cf-Access-Jwt-Assertion` JWT.
|
|
4
|
-
*
|
|
5
|
-
* Extends the standard `JWTPayload` (`iss`/`aud`/`sub`/`exp`/`iat`/…) with the
|
|
6
|
-
* Access-specific fields. Which optional fields are present depends on the
|
|
7
|
-
* caller and the Access application config. SSO users carry `email` (and
|
|
8
|
-
* `groups` when the policy emits them), with `sub` as the stable user id.
|
|
9
|
-
* Service tokens carry `common_name` and an empty `sub`; there is no `email`.
|
|
10
|
-
*
|
|
11
|
-
* Cloudflare may add further custom claims — they pass through verbatim via the
|
|
12
|
-
* index signature so the claims stay a faithful view of the token.
|
|
13
|
-
*/
|
|
14
|
-
interface AccessClaims extends JWTPayload {
|
|
15
|
-
/** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
|
|
16
|
-
common_name?: string;
|
|
17
|
-
/** ISO-3166-1 alpha-2 country the request was authorized from, when available. */
|
|
18
|
-
country?: string;
|
|
19
|
-
/** Verified user email. Present for interactive (SSO) callers. */
|
|
20
|
-
email?: string;
|
|
21
|
-
/** Identity-provider group memberships, when the Access policy is configured to emit them. */
|
|
22
|
-
groups?: string[];
|
|
23
|
-
/** Per-session nonce Cloudflare rotates on re-authentication. */
|
|
24
|
-
identity_nonce?: string;
|
|
25
|
-
/** Token kind, e.g. `"app"`. */
|
|
26
|
-
type?: string;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* The minimal `resolveIdentity` return contract shared with `@lunora/runtime`'s
|
|
30
|
-
* `WorkerOptions.resolveIdentity` (`ResolvedIdentity`). Declared structurally so
|
|
31
|
-
* this package takes no runtime dependency on `@lunora/runtime`; the value is
|
|
32
|
-
* assignable to the runtime hook.
|
|
33
|
-
*
|
|
34
|
-
* `userId` becomes `ctx.auth.userId`; every other key is forwarded (server-side,
|
|
35
|
-
* unforgeable) into `x-lunora-identity` and surfaced via `ctx.auth.getIdentity()`.
|
|
36
|
-
* `exp` (JWT epoch **seconds**) drives WebSocket credential expiry — omit it and
|
|
37
|
-
* a live subscription socket never expires.
|
|
38
|
-
*/
|
|
39
|
-
interface ResolvedIdentityLike {
|
|
40
|
-
/** All other claims pass through into `ctx.auth.getIdentity()`. */
|
|
41
|
-
[claim: string]: unknown;
|
|
42
|
-
/** JWT `exp` in epoch **seconds** (NOT milliseconds). Drives WS socket expiry. */
|
|
43
|
-
exp?: number;
|
|
44
|
-
/** Absolute expiry in epoch **milliseconds**. Alternative to `exp`; takes precedence in the runtime. */
|
|
45
|
-
expiresAtMs?: number;
|
|
46
|
-
/** The stable caller id. Becomes `ctx.auth.userId` and what `serverDefault(({auth}) => auth.userId)` stamps. */
|
|
47
|
-
userId: string;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* The verified Access identity produced by `createAccessResolver`. A
|
|
51
|
-
* {@link ResolvedIdentityLike} with the commonly-used Access claims promoted to
|
|
52
|
-
* named, camelCased fields (so policies read `auth.identity.groups` etc.) plus
|
|
53
|
-
* the full raw claim set under `access` for fidelity.
|
|
54
|
-
*/
|
|
55
|
-
interface ResolvedAccessIdentity extends ResolvedIdentityLike {
|
|
56
|
-
/** The full, verified claim set (snake_cased wire names preserved). */
|
|
57
|
-
access: AccessClaims;
|
|
58
|
-
/** Service-token name (`common_name`), for machine callers. */
|
|
59
|
-
commonName?: string;
|
|
60
|
-
/** Verified email, for SSO callers. */
|
|
61
|
-
email?: string;
|
|
62
|
-
/** IdP group memberships, when emitted by the Access policy. */
|
|
63
|
-
groups?: string[];
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* A key source for `verifyAccessJwt`. Either a `jose` remote/local JWKS getter,
|
|
67
|
-
* or a single public key (handy for tests that mint their own RS256 tokens).
|
|
68
|
-
* When omitted, a cached remote JWKS is built from `teamDomain`.
|
|
69
|
-
*/
|
|
70
|
-
type AccessKeySet = CryptoKey | JWTVerifyGetKey | KeyObject | Uint8Array;
|
|
71
|
-
/** Options for `verifyAccessJwt`. */
|
|
72
|
-
interface VerifyAccessJwtOptions {
|
|
73
|
-
/**
|
|
74
|
-
* The Access application **AUD tag(s)** (the application audience from the
|
|
75
|
-
* Access app's Overview). Verification rejects a token whose `aud` does not
|
|
76
|
-
* include one of these — this is what scopes a token to *your* app.
|
|
77
|
-
*/
|
|
78
|
-
aud: string | string[];
|
|
79
|
-
/** Clock-skew tolerance in **seconds** applied to `exp`/`nbf`/`iat`. Default `0`. */
|
|
80
|
-
clockToleranceSec?: number;
|
|
81
|
-
/**
|
|
82
|
-
* Override the verification key source. Primarily for tests; in production
|
|
83
|
-
* leave unset to use the cached remote JWKS derived from `teamDomain`.
|
|
84
|
-
*/
|
|
85
|
-
keySet?: AccessKeySet;
|
|
86
|
-
/**
|
|
87
|
-
* Your Cloudflare Access team domain. Accepts the short team name (`acme`),
|
|
88
|
-
* the host (`acme.cloudflareaccess.com`), or a full URL
|
|
89
|
-
* (`https://acme.cloudflareaccess.com`). Determines both the expected issuer
|
|
90
|
-
* and the JWKS endpoint.
|
|
91
|
-
*/
|
|
92
|
-
teamDomain: string;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Common options for the request-driven Access primitives — how to read the JWT
|
|
96
|
-
* off the request and what to do when verification fails. Shared by
|
|
97
|
-
* {@link CreateAccessResolverOptions} and `AccessAdminGateOptions`, which add
|
|
98
|
-
* their distinct mapping / authorization step on top.
|
|
99
|
-
*/
|
|
100
|
-
interface RequestVerifyOptions extends VerifyAccessJwtOptions {
|
|
101
|
-
/**
|
|
102
|
-
* Cookie name carrying the Access JWT when the header is absent (browser
|
|
103
|
-
* navigations). Default `"CF_Authorization"`.
|
|
104
|
-
*/
|
|
105
|
-
cookieName?: string;
|
|
106
|
-
/**
|
|
107
|
-
* Request header carrying the Access JWT. Default `"cf-access-jwt-assertion"`
|
|
108
|
-
* (matched case-insensitively).
|
|
109
|
-
*/
|
|
110
|
-
headerName?: string;
|
|
111
|
-
/**
|
|
112
|
-
* Invoked when a token is present but fails verification (bad signature,
|
|
113
|
-
* wrong audience, expired, …). The caller still fails closed (resolver
|
|
114
|
-
* returns `null`, admin gate returns `false`); this is your hook to
|
|
115
|
-
* log/observe. It is **not** called when no token is present at all.
|
|
116
|
-
*/
|
|
117
|
-
onError?: (error: unknown, request: Request) => void;
|
|
118
|
-
}
|
|
119
|
-
/** Options for `createAccessResolver`; extends {@link RequestVerifyOptions}. */
|
|
120
|
-
interface CreateAccessResolverOptions extends RequestVerifyOptions {
|
|
121
|
-
/**
|
|
122
|
-
* Remap verified claims into the resolved identity. Return an object to
|
|
123
|
-
* shallow-merge over the defaults; return a `userId` to override the derived
|
|
124
|
-
* caller id. Runs only after signature/issuer/audience/expiry are verified.
|
|
125
|
-
*/
|
|
126
|
-
mapClaims?: (claims: AccessClaims) => Record<string, unknown>;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* A `resolveIdentity`-shaped function: maps an inbound request to a verified
|
|
130
|
-
* identity (or `null` for anonymous). Assignable to `@lunora/runtime`'s
|
|
131
|
-
* `WorkerOptions.resolveIdentity`.
|
|
132
|
-
*/
|
|
133
|
-
type ResolveIdentityFunction = (request: Request, env?: unknown) => (ResolvedIdentityLike | null) | Promise<ResolvedIdentityLike | null>;
|
|
134
|
-
export { AccessClaims as A, CreateAccessResolverOptions as C, RequestVerifyOptions as R, VerifyAccessJwtOptions as V, ResolveIdentityFunction as a, AccessKeySet as b, ResolvedAccessIdentity as c, ResolvedIdentityLike as d };
|