@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.
- package/LICENSE.md +340 -0
- package/dist/admin.d.mts +57 -30
- package/dist/admin.d.ts +57 -30
- package/dist/admin.mjs +1 -8
- package/dist/context.d.mts +45 -46
- package/dist/context.d.ts +45 -46
- package/dist/context.mjs +1 -36
- package/dist/index.d.mts +67 -62
- package/dist/index.d.ts +67 -62
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/accessIssuer-CLO6p5MF.mjs +1 -0
- package/dist/packem_shared/composeResolvers-JBsWQvv4.mjs +1 -0
- package/dist/packem_shared/identity-groups-CCucbpzP.mjs +1 -0
- package/dist/packem_shared/index.d-D7R8k7yf.d.mts +21 -0
- package/dist/packem_shared/index.d-D7R8k7yf.d.ts +21 -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/dist/roles.d.mts +32 -33
- package/dist/roles.d.ts +32 -33
- package/dist/roles.mjs +1 -35
- package/package.json +4 -4
- package/dist/packem_shared/accessIssuer-83KYwaCp.mjs +0 -93
- package/dist/packem_shared/composeResolvers-BI3envaN.mjs +0 -38
- package/dist/packem_shared/index.d-ByAnpUzP.d.mts +0 -23
- package/dist/packem_shared/index.d-ByAnpUzP.d.ts +0 -23
- package/dist/packem_shared/types.d-BO8d74KI.d.mts +0 -134
- package/dist/packem_shared/types.d-BO8d74KI.d.ts +0 -134
package/dist/context.d.mts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { M as Middleware } from "./packem_shared/index.d-
|
|
2
|
-
import { A as AccessClaims } from "./packem_shared/types.d-
|
|
3
|
-
import '@lunora/errors';
|
|
1
|
+
import { M as Middleware } from "./packem_shared/index.d-D7R8k7yf.mjs";
|
|
2
|
+
import { A as AccessClaims } from "./packem_shared/types.d-D30o96f7.mjs";
|
|
4
3
|
import 'jose';
|
|
5
4
|
/**
|
|
6
|
-
* The slice of context {@link accessContext} reads: the `auth` facade every
|
|
7
|
-
* Lunora ctx carries. `getIdentity()` returns the verified identity envelope —
|
|
8
|
-
* `createAccessResolver`'s {@link import("./types").ResolvedAccessIdentity}
|
|
9
|
-
* output, which carries the full claim set under `access` plus the promoted
|
|
10
|
-
* `email` / `groups` / `commonName` fields.
|
|
11
|
-
*/
|
|
5
|
+
* The slice of context {@link accessContext} reads: the `auth` facade every
|
|
6
|
+
* Lunora ctx carries. `getIdentity()` returns the verified identity envelope —
|
|
7
|
+
* `createAccessResolver`'s {@link import("./types").ResolvedAccessIdentity}
|
|
8
|
+
* output, which carries the full claim set under `access` plus the promoted
|
|
9
|
+
* `email` / `groups` / `commonName` fields.
|
|
10
|
+
*/
|
|
12
11
|
interface AccessContextInput {
|
|
13
12
|
auth?: {
|
|
14
13
|
getIdentity?: () => (Record<string, unknown> | null) | Promise<Record<string, unknown> | null>;
|
|
@@ -16,11 +15,11 @@ interface AccessContextInput {
|
|
|
16
15
|
};
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
19
|
-
* The typed, per-request `ctx.access` facade {@link accessContext} attaches. A
|
|
20
|
-
* synchronous, Access-shaped read over the already-resolved identity — so a
|
|
21
|
-
* handler reads `ctx.access.email` / `ctx.access.hasGroup("ops")` without an
|
|
22
|
-
* `await` or a cast off the generic `ctx.auth.getIdentity()` envelope.
|
|
23
|
-
*/
|
|
18
|
+
* The typed, per-request `ctx.access` facade {@link accessContext} attaches. A
|
|
19
|
+
* synchronous, Access-shaped read over the already-resolved identity — so a
|
|
20
|
+
* handler reads `ctx.access.email` / `ctx.access.hasGroup("ops")` without an
|
|
21
|
+
* `await` or a cast off the generic `ctx.auth.getIdentity()` envelope.
|
|
22
|
+
*/
|
|
24
23
|
interface AccessFacade {
|
|
25
24
|
/** True when a verified Access identity is present on the request. */
|
|
26
25
|
readonly authenticated: boolean;
|
|
@@ -42,39 +41,39 @@ interface AccessContextOutput extends AccessContextInput {
|
|
|
42
41
|
access: AccessFacade;
|
|
43
42
|
}
|
|
44
43
|
/**
|
|
45
|
-
* Build the `ctx.access` facade from a (possibly absent) resolved identity
|
|
46
|
-
* envelope. Returns the anonymous facade when no identity is present, so callers
|
|
47
|
-
* never null-check. Shared by {@link accessContext} and the codegen-wired global
|
|
48
|
-
* `ctx.access` (which calls this synchronously from the resolved identity locals
|
|
49
|
-
* at ctx-build time, so a global `ctx.access` adds only this object construction
|
|
50
|
-
* per request — no extra I/O or re-verification).
|
|
51
|
-
*/
|
|
44
|
+
* Build the `ctx.access` facade from a (possibly absent) resolved identity
|
|
45
|
+
* envelope. Returns the anonymous facade when no identity is present, so callers
|
|
46
|
+
* never null-check. Shared by {@link accessContext} and the codegen-wired global
|
|
47
|
+
* `ctx.access` (which calls this synchronously from the resolved identity locals
|
|
48
|
+
* at ctx-build time, so a global `ctx.access` adds only this object construction
|
|
49
|
+
* per request — no extra I/O or re-verification).
|
|
50
|
+
*/
|
|
52
51
|
declare const accessFacade: (identity: Record<string, unknown> | null | undefined, userId: string | null | undefined) => AccessFacade;
|
|
53
52
|
/**
|
|
54
|
-
* Middleware that attaches a typed `ctx.access` facade derived from the verified
|
|
55
|
-
* Cloudflare Access identity. It resolves `ctx.auth.getIdentity()` once and
|
|
56
|
-
* exposes a **synchronous**, Access-shaped read — `ctx.access.email`,
|
|
57
|
-
* `ctx.access.groups`, `ctx.access.hasGroup("ops")`, `ctx.access.claims` — so a
|
|
58
|
-
* handler reads the verified identity ergonomically and with full typing instead
|
|
59
|
-
* of casting off the generic `getIdentity()` envelope.
|
|
60
|
-
*
|
|
61
|
-
* When no identity is resolved (anonymous request) it attaches the anonymous
|
|
62
|
-
* facade — `authenticated: false`, empty `groups`, `hasGroup` always `false` —
|
|
63
|
-
* so reads stay safe without a null check, and authorization decisions still
|
|
64
|
-
* fail closed.
|
|
65
|
-
*
|
|
66
|
-
* It does not gate the request; pair it with `rls(...)` (or
|
|
67
|
-
* `accessRoles(...)` → `rls(...)`) when you need enforcement. It only surfaces
|
|
68
|
-
* the identity for branching inside a handler.
|
|
69
|
-
*
|
|
70
|
-
* ```ts
|
|
71
|
-
* export const whoAmI = query
|
|
72
|
-
* .use(accessContext())
|
|
73
|
-
* .query(async ({ ctx }) => ({
|
|
74
|
-
* email: ctx.access.email,
|
|
75
|
-
* isOps: ctx.access.hasGroup("ops"),
|
|
76
|
-
* }));
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
53
|
+
* Middleware that attaches a typed `ctx.access` facade derived from the verified
|
|
54
|
+
* Cloudflare Access identity. It resolves `ctx.auth.getIdentity()` once and
|
|
55
|
+
* exposes a **synchronous**, Access-shaped read — `ctx.access.email`,
|
|
56
|
+
* `ctx.access.groups`, `ctx.access.hasGroup("ops")`, `ctx.access.claims` — so a
|
|
57
|
+
* handler reads the verified identity ergonomically and with full typing instead
|
|
58
|
+
* of casting off the generic `getIdentity()` envelope.
|
|
59
|
+
*
|
|
60
|
+
* When no identity is resolved (anonymous request) it attaches the anonymous
|
|
61
|
+
* facade — `authenticated: false`, empty `groups`, `hasGroup` always `false` —
|
|
62
|
+
* so reads stay safe without a null check, and authorization decisions still
|
|
63
|
+
* fail closed.
|
|
64
|
+
*
|
|
65
|
+
* It does not gate the request; pair it with `rls(...)` (or
|
|
66
|
+
* `accessRoles(...)` → `rls(...)`) when you need enforcement. It only surfaces
|
|
67
|
+
* the identity for branching inside a handler.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* export const whoAmI = query
|
|
71
|
+
* .use(accessContext())
|
|
72
|
+
* .query(async ({ ctx }) => ({
|
|
73
|
+
* email: ctx.access.email,
|
|
74
|
+
* isOps: ctx.access.hasGroup("ops"),
|
|
75
|
+
* }));
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
79
78
|
declare const accessContext: <Context extends AccessContextInput>() => Middleware<Context, AccessContextOutput & Context>;
|
|
80
79
|
export { type AccessContextInput, type AccessContextOutput, type AccessFacade, accessContext, accessFacade };
|
package/dist/context.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { M as Middleware } from "./packem_shared/index.d-
|
|
2
|
-
import { A as AccessClaims } from "./packem_shared/types.d-
|
|
3
|
-
import '@lunora/errors';
|
|
1
|
+
import { M as Middleware } from "./packem_shared/index.d-D7R8k7yf.js";
|
|
2
|
+
import { A as AccessClaims } from "./packem_shared/types.d-D30o96f7.js";
|
|
4
3
|
import 'jose';
|
|
5
4
|
/**
|
|
6
|
-
* The slice of context {@link accessContext} reads: the `auth` facade every
|
|
7
|
-
* Lunora ctx carries. `getIdentity()` returns the verified identity envelope —
|
|
8
|
-
* `createAccessResolver`'s {@link import("./types").ResolvedAccessIdentity}
|
|
9
|
-
* output, which carries the full claim set under `access` plus the promoted
|
|
10
|
-
* `email` / `groups` / `commonName` fields.
|
|
11
|
-
*/
|
|
5
|
+
* The slice of context {@link accessContext} reads: the `auth` facade every
|
|
6
|
+
* Lunora ctx carries. `getIdentity()` returns the verified identity envelope —
|
|
7
|
+
* `createAccessResolver`'s {@link import("./types").ResolvedAccessIdentity}
|
|
8
|
+
* output, which carries the full claim set under `access` plus the promoted
|
|
9
|
+
* `email` / `groups` / `commonName` fields.
|
|
10
|
+
*/
|
|
12
11
|
interface AccessContextInput {
|
|
13
12
|
auth?: {
|
|
14
13
|
getIdentity?: () => (Record<string, unknown> | null) | Promise<Record<string, unknown> | null>;
|
|
@@ -16,11 +15,11 @@ interface AccessContextInput {
|
|
|
16
15
|
};
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
19
|
-
* The typed, per-request `ctx.access` facade {@link accessContext} attaches. A
|
|
20
|
-
* synchronous, Access-shaped read over the already-resolved identity — so a
|
|
21
|
-
* handler reads `ctx.access.email` / `ctx.access.hasGroup("ops")` without an
|
|
22
|
-
* `await` or a cast off the generic `ctx.auth.getIdentity()` envelope.
|
|
23
|
-
*/
|
|
18
|
+
* The typed, per-request `ctx.access` facade {@link accessContext} attaches. A
|
|
19
|
+
* synchronous, Access-shaped read over the already-resolved identity — so a
|
|
20
|
+
* handler reads `ctx.access.email` / `ctx.access.hasGroup("ops")` without an
|
|
21
|
+
* `await` or a cast off the generic `ctx.auth.getIdentity()` envelope.
|
|
22
|
+
*/
|
|
24
23
|
interface AccessFacade {
|
|
25
24
|
/** True when a verified Access identity is present on the request. */
|
|
26
25
|
readonly authenticated: boolean;
|
|
@@ -42,39 +41,39 @@ interface AccessContextOutput extends AccessContextInput {
|
|
|
42
41
|
access: AccessFacade;
|
|
43
42
|
}
|
|
44
43
|
/**
|
|
45
|
-
* Build the `ctx.access` facade from a (possibly absent) resolved identity
|
|
46
|
-
* envelope. Returns the anonymous facade when no identity is present, so callers
|
|
47
|
-
* never null-check. Shared by {@link accessContext} and the codegen-wired global
|
|
48
|
-
* `ctx.access` (which calls this synchronously from the resolved identity locals
|
|
49
|
-
* at ctx-build time, so a global `ctx.access` adds only this object construction
|
|
50
|
-
* per request — no extra I/O or re-verification).
|
|
51
|
-
*/
|
|
44
|
+
* Build the `ctx.access` facade from a (possibly absent) resolved identity
|
|
45
|
+
* envelope. Returns the anonymous facade when no identity is present, so callers
|
|
46
|
+
* never null-check. Shared by {@link accessContext} and the codegen-wired global
|
|
47
|
+
* `ctx.access` (which calls this synchronously from the resolved identity locals
|
|
48
|
+
* at ctx-build time, so a global `ctx.access` adds only this object construction
|
|
49
|
+
* per request — no extra I/O or re-verification).
|
|
50
|
+
*/
|
|
52
51
|
declare const accessFacade: (identity: Record<string, unknown> | null | undefined, userId: string | null | undefined) => AccessFacade;
|
|
53
52
|
/**
|
|
54
|
-
* Middleware that attaches a typed `ctx.access` facade derived from the verified
|
|
55
|
-
* Cloudflare Access identity. It resolves `ctx.auth.getIdentity()` once and
|
|
56
|
-
* exposes a **synchronous**, Access-shaped read — `ctx.access.email`,
|
|
57
|
-
* `ctx.access.groups`, `ctx.access.hasGroup("ops")`, `ctx.access.claims` — so a
|
|
58
|
-
* handler reads the verified identity ergonomically and with full typing instead
|
|
59
|
-
* of casting off the generic `getIdentity()` envelope.
|
|
60
|
-
*
|
|
61
|
-
* When no identity is resolved (anonymous request) it attaches the anonymous
|
|
62
|
-
* facade — `authenticated: false`, empty `groups`, `hasGroup` always `false` —
|
|
63
|
-
* so reads stay safe without a null check, and authorization decisions still
|
|
64
|
-
* fail closed.
|
|
65
|
-
*
|
|
66
|
-
* It does not gate the request; pair it with `rls(...)` (or
|
|
67
|
-
* `accessRoles(...)` → `rls(...)`) when you need enforcement. It only surfaces
|
|
68
|
-
* the identity for branching inside a handler.
|
|
69
|
-
*
|
|
70
|
-
* ```ts
|
|
71
|
-
* export const whoAmI = query
|
|
72
|
-
* .use(accessContext())
|
|
73
|
-
* .query(async ({ ctx }) => ({
|
|
74
|
-
* email: ctx.access.email,
|
|
75
|
-
* isOps: ctx.access.hasGroup("ops"),
|
|
76
|
-
* }));
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
53
|
+
* Middleware that attaches a typed `ctx.access` facade derived from the verified
|
|
54
|
+
* Cloudflare Access identity. It resolves `ctx.auth.getIdentity()` once and
|
|
55
|
+
* exposes a **synchronous**, Access-shaped read — `ctx.access.email`,
|
|
56
|
+
* `ctx.access.groups`, `ctx.access.hasGroup("ops")`, `ctx.access.claims` — so a
|
|
57
|
+
* handler reads the verified identity ergonomically and with full typing instead
|
|
58
|
+
* of casting off the generic `getIdentity()` envelope.
|
|
59
|
+
*
|
|
60
|
+
* When no identity is resolved (anonymous request) it attaches the anonymous
|
|
61
|
+
* facade — `authenticated: false`, empty `groups`, `hasGroup` always `false` —
|
|
62
|
+
* so reads stay safe without a null check, and authorization decisions still
|
|
63
|
+
* fail closed.
|
|
64
|
+
*
|
|
65
|
+
* It does not gate the request; pair it with `rls(...)` (or
|
|
66
|
+
* `accessRoles(...)` → `rls(...)`) when you need enforcement. It only surfaces
|
|
67
|
+
* the identity for branching inside a handler.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* export const whoAmI = query
|
|
71
|
+
* .use(accessContext())
|
|
72
|
+
* .query(async ({ ctx }) => ({
|
|
73
|
+
* email: ctx.access.email,
|
|
74
|
+
* isOps: ctx.access.hasGroup("ops"),
|
|
75
|
+
* }));
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
79
78
|
declare const accessContext: <Context extends AccessContextInput>() => Middleware<Context, AccessContextOutput & Context>;
|
|
80
79
|
export { type AccessContextInput, type AccessContextOutput, type AccessFacade, accessContext, accessFacade };
|
package/dist/context.mjs
CHANGED
|
@@ -1,36 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const stringClaim = (value) => typeof value === "string" ? value : void 0;
|
|
3
|
-
const ANONYMOUS_FACADE = {
|
|
4
|
-
authenticated: false,
|
|
5
|
-
claims: void 0,
|
|
6
|
-
commonName: void 0,
|
|
7
|
-
email: void 0,
|
|
8
|
-
groups: [],
|
|
9
|
-
hasGroup: () => false,
|
|
10
|
-
userId: void 0
|
|
11
|
-
};
|
|
12
|
-
const facadeFor = (identity, userId) => {
|
|
13
|
-
const raw = identity["access"];
|
|
14
|
-
if (typeof raw !== "object" || raw === null) {
|
|
15
|
-
return ANONYMOUS_FACADE;
|
|
16
|
-
}
|
|
17
|
-
const claims = raw;
|
|
18
|
-
const groups = stringList(identity["groups"] ?? claims.groups);
|
|
19
|
-
return {
|
|
20
|
-
authenticated: true,
|
|
21
|
-
claims,
|
|
22
|
-
commonName: stringClaim(identity["commonName"]) ?? stringClaim(claims.common_name),
|
|
23
|
-
email: stringClaim(identity["email"]) ?? stringClaim(claims.email),
|
|
24
|
-
groups,
|
|
25
|
-
hasGroup: (group) => groups.includes(group),
|
|
26
|
-
userId
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
const accessFacade = (identity, userId) => identity ? facadeFor(identity, userId ?? void 0) : ANONYMOUS_FACADE;
|
|
30
|
-
const accessContext = () => async ({ ctx, next }) => {
|
|
31
|
-
const identity = await ctx.auth?.getIdentity?.();
|
|
32
|
-
const access = accessFacade(identity, ctx.auth?.userId);
|
|
33
|
-
return next({ ctx: { access } });
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export { accessContext, accessFacade };
|
|
1
|
+
import{r as m}from"./packem_shared/identity-groups-CCucbpzP.mjs";const c=o=>typeof o=="string"?o:void 0,n={authenticated:!1,claims:void 0,commonName:void 0,email:void 0,groups:[],hasGroup:()=>!1,userId:void 0},i=(o,a)=>{const s=o.access;if(typeof s!="object"||s===null)return n;const e=s,t=m(o)??[];return{authenticated:!0,claims:e,commonName:c(o.commonName)??c(e.common_name),email:c(o.email)??c(e.email),groups:t,hasGroup:r=>t.includes(r),userId:a}},u=(o,a)=>o?i(o,a??void 0):n,l=()=>async({ctx:o,next:a})=>{const s=await o.auth?.getIdentity?.(),e=u(s,o.auth?.userId);return a({ctx:{access:e}})};export{l as accessContext,u as accessFacade};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,71 +1,76 @@
|
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
8
|
+
*
|
|
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.
|
|
29
|
+
*
|
|
30
|
+
* Wire it in your worker entry:
|
|
31
|
+
*
|
|
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.
|
|
37
|
+
* options.resolveIdentity = createAccessResolver({
|
|
38
|
+
* teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
|
|
39
|
+
* aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare const createAccessResolver: (options?: CreateAccessResolverOptions) => ResolveIdentityFunction;
|
|
26
44
|
/**
|
|
27
|
-
* Compose several `resolveIdentity` adapters into one: each is tried in order
|
|
28
|
-
* and the first to return a non-null identity wins. The canonical use is
|
|
29
|
-
* pairing Access with `@lunora/auth` —
|
|
30
|
-
* `composeResolvers(accessResolver, betterAuthResolver)` — so a request carrying
|
|
31
|
-
* an Access JWT (machine/SSO) is authenticated by Access while everyone else
|
|
32
|
-
* falls through to the app's own session.
|
|
33
|
-
*/
|
|
45
|
+
* Compose several `resolveIdentity` adapters into one: each is tried in order
|
|
46
|
+
* and the first to return a non-null identity wins. The canonical use is
|
|
47
|
+
* pairing Access with `@lunora/auth` —
|
|
48
|
+
* `composeResolvers(accessResolver, betterAuthResolver)` — so a request carrying
|
|
49
|
+
* an Access JWT (machine/SSO) is authenticated by Access while everyone else
|
|
50
|
+
* falls through to the app's own session.
|
|
51
|
+
*/
|
|
34
52
|
declare const composeResolvers: (...resolvers: ResolveIdentityFunction[]) => ResolveIdentityFunction;
|
|
35
53
|
/**
|
|
36
|
-
* Normalize a configured team domain to the canonical Access issuer URL.
|
|
37
|
-
*
|
|
38
|
-
* Accepts a short team name (`acme`), a host (`acme.cloudflareaccess.com`), or a
|
|
39
|
-
* full URL, and always returns an `https://` origin with no trailing slash. A
|
|
40
|
-
* bare name with no dot is expanded to the `cloudflareaccess.com` host.
|
|
41
|
-
*/
|
|
54
|
+
* Normalize a configured team domain to the canonical Access issuer URL.
|
|
55
|
+
*
|
|
56
|
+
* Accepts a short team name (`acme`), a host (`acme.cloudflareaccess.com`), or a
|
|
57
|
+
* full URL, and always returns an `https://` origin with no trailing slash. A
|
|
58
|
+
* bare name with no dot is expanded to the `cloudflareaccess.com` host.
|
|
59
|
+
*/
|
|
42
60
|
declare const accessIssuer: (teamDomain: string) => string;
|
|
43
61
|
/**
|
|
44
|
-
* Verify a Cloudflare Access JWT and return its claims.
|
|
45
|
-
*
|
|
46
|
-
* Enforces, in one shot: RS256 signature against the team JWKS, `iss` equal to
|
|
47
|
-
* the team issuer, `aud` containing one of the configured Access application AUD
|
|
48
|
-
* tags, and a non-expired `exp` (with optional clock tolerance). The algorithm
|
|
49
|
-
* is pinned to `RS256` so an `alg:none` or HS-signed forgery is rejected
|
|
50
|
-
* outright.
|
|
51
|
-
*
|
|
52
|
-
* Throws (a `jose` error) on any failure — callers that want fail-closed
|
|
53
|
-
* anonymous behaviour should catch and treat it as "no identity" (the
|
|
54
|
-
* `createAccessResolver` adapter does exactly this).
|
|
55
|
-
* @param token The raw compact JWT (header value or cookie value).
|
|
56
|
-
*/
|
|
62
|
+
* Verify a Cloudflare Access JWT and return its claims.
|
|
63
|
+
*
|
|
64
|
+
* Enforces, in one shot: RS256 signature against the team JWKS, `iss` equal to
|
|
65
|
+
* the team issuer, `aud` containing one of the configured Access application AUD
|
|
66
|
+
* tags, and a non-expired `exp` (with optional clock tolerance). The algorithm
|
|
67
|
+
* is pinned to `RS256` so an `alg:none` or HS-signed forgery is rejected
|
|
68
|
+
* outright.
|
|
69
|
+
*
|
|
70
|
+
* Throws (a `jose` error) on any failure — callers that want fail-closed
|
|
71
|
+
* anonymous behaviour should catch and treat it as "no identity" (the
|
|
72
|
+
* `createAccessResolver` adapter does exactly this).
|
|
73
|
+
* @param token The raw compact JWT (header value or cookie value).
|
|
74
|
+
*/
|
|
57
75
|
declare const verifyAccessJwt: (token: string, options: VerifyAccessJwtOptions) => Promise<AccessClaims>;
|
|
58
|
-
/**
|
|
59
|
-
* Read the Access JWT off a request and verify it. Returns the verified claims,
|
|
60
|
-
* or `undefined` when no token is present **or** verification fails — the single
|
|
61
|
-
* fail-closed "no Access identity" signal that both `createAccessResolver` and
|
|
62
|
-
* `accessAdminGate` build their distinct mapping / authorization step on top of.
|
|
63
|
-
*
|
|
64
|
-
* This is the package's one place that turns a request into verified claims:
|
|
65
|
-
* header/cookie default resolution, the {@link readToken} read, the
|
|
66
|
-
* {@link verifyAccessJwt} call, and the `onError`-observed fail-closed catch all
|
|
67
|
-
* live here so the resolver and the admin gate carry only their genuinely
|
|
68
|
-
* distinct line. `onError` fires for a present-but-invalid token, never for an
|
|
69
|
-
* absent one.
|
|
70
|
-
*/
|
|
71
76
|
export { type AccessClaims, type CreateAccessResolverOptions, type ResolveIdentityFunction, type VerifyAccessJwtOptions, accessIssuer, composeResolvers, createAccessResolver, verifyAccessJwt };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,71 +1,76 @@
|
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
8
|
+
*
|
|
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.
|
|
29
|
+
*
|
|
30
|
+
* Wire it in your worker entry:
|
|
31
|
+
*
|
|
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.
|
|
37
|
+
* options.resolveIdentity = createAccessResolver({
|
|
38
|
+
* teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
|
|
39
|
+
* aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare const createAccessResolver: (options?: CreateAccessResolverOptions) => ResolveIdentityFunction;
|
|
26
44
|
/**
|
|
27
|
-
* Compose several `resolveIdentity` adapters into one: each is tried in order
|
|
28
|
-
* and the first to return a non-null identity wins. The canonical use is
|
|
29
|
-
* pairing Access with `@lunora/auth` —
|
|
30
|
-
* `composeResolvers(accessResolver, betterAuthResolver)` — so a request carrying
|
|
31
|
-
* an Access JWT (machine/SSO) is authenticated by Access while everyone else
|
|
32
|
-
* falls through to the app's own session.
|
|
33
|
-
*/
|
|
45
|
+
* Compose several `resolveIdentity` adapters into one: each is tried in order
|
|
46
|
+
* and the first to return a non-null identity wins. The canonical use is
|
|
47
|
+
* pairing Access with `@lunora/auth` —
|
|
48
|
+
* `composeResolvers(accessResolver, betterAuthResolver)` — so a request carrying
|
|
49
|
+
* an Access JWT (machine/SSO) is authenticated by Access while everyone else
|
|
50
|
+
* falls through to the app's own session.
|
|
51
|
+
*/
|
|
34
52
|
declare const composeResolvers: (...resolvers: ResolveIdentityFunction[]) => ResolveIdentityFunction;
|
|
35
53
|
/**
|
|
36
|
-
* Normalize a configured team domain to the canonical Access issuer URL.
|
|
37
|
-
*
|
|
38
|
-
* Accepts a short team name (`acme`), a host (`acme.cloudflareaccess.com`), or a
|
|
39
|
-
* full URL, and always returns an `https://` origin with no trailing slash. A
|
|
40
|
-
* bare name with no dot is expanded to the `cloudflareaccess.com` host.
|
|
41
|
-
*/
|
|
54
|
+
* Normalize a configured team domain to the canonical Access issuer URL.
|
|
55
|
+
*
|
|
56
|
+
* Accepts a short team name (`acme`), a host (`acme.cloudflareaccess.com`), or a
|
|
57
|
+
* full URL, and always returns an `https://` origin with no trailing slash. A
|
|
58
|
+
* bare name with no dot is expanded to the `cloudflareaccess.com` host.
|
|
59
|
+
*/
|
|
42
60
|
declare const accessIssuer: (teamDomain: string) => string;
|
|
43
61
|
/**
|
|
44
|
-
* Verify a Cloudflare Access JWT and return its claims.
|
|
45
|
-
*
|
|
46
|
-
* Enforces, in one shot: RS256 signature against the team JWKS, `iss` equal to
|
|
47
|
-
* the team issuer, `aud` containing one of the configured Access application AUD
|
|
48
|
-
* tags, and a non-expired `exp` (with optional clock tolerance). The algorithm
|
|
49
|
-
* is pinned to `RS256` so an `alg:none` or HS-signed forgery is rejected
|
|
50
|
-
* outright.
|
|
51
|
-
*
|
|
52
|
-
* Throws (a `jose` error) on any failure — callers that want fail-closed
|
|
53
|
-
* anonymous behaviour should catch and treat it as "no identity" (the
|
|
54
|
-
* `createAccessResolver` adapter does exactly this).
|
|
55
|
-
* @param token The raw compact JWT (header value or cookie value).
|
|
56
|
-
*/
|
|
62
|
+
* Verify a Cloudflare Access JWT and return its claims.
|
|
63
|
+
*
|
|
64
|
+
* Enforces, in one shot: RS256 signature against the team JWKS, `iss` equal to
|
|
65
|
+
* the team issuer, `aud` containing one of the configured Access application AUD
|
|
66
|
+
* tags, and a non-expired `exp` (with optional clock tolerance). The algorithm
|
|
67
|
+
* is pinned to `RS256` so an `alg:none` or HS-signed forgery is rejected
|
|
68
|
+
* outright.
|
|
69
|
+
*
|
|
70
|
+
* Throws (a `jose` error) on any failure — callers that want fail-closed
|
|
71
|
+
* anonymous behaviour should catch and treat it as "no identity" (the
|
|
72
|
+
* `createAccessResolver` adapter does exactly this).
|
|
73
|
+
* @param token The raw compact JWT (header value or cookie value).
|
|
74
|
+
*/
|
|
57
75
|
declare const verifyAccessJwt: (token: string, options: VerifyAccessJwtOptions) => Promise<AccessClaims>;
|
|
58
|
-
/**
|
|
59
|
-
* Read the Access JWT off a request and verify it. Returns the verified claims,
|
|
60
|
-
* or `undefined` when no token is present **or** verification fails — the single
|
|
61
|
-
* fail-closed "no Access identity" signal that both `createAccessResolver` and
|
|
62
|
-
* `accessAdminGate` build their distinct mapping / authorization step on top of.
|
|
63
|
-
*
|
|
64
|
-
* This is the package's one place that turns a request into verified claims:
|
|
65
|
-
* header/cookie default resolution, the {@link readToken} read, the
|
|
66
|
-
* {@link verifyAccessJwt} call, and the `onError`-observed fail-closed catch all
|
|
67
|
-
* live here so the resolver and the admin gate carry only their genuinely
|
|
68
|
-
* distinct line. `onError` fires for a present-but-invalid token, never for an
|
|
69
|
-
* absent one.
|
|
70
|
-
*/
|
|
71
76
|
export { type AccessClaims, type CreateAccessResolverOptions, type ResolveIdentityFunction, type VerifyAccessJwtOptions, accessIssuer, composeResolvers, createAccessResolver, verifyAccessJwt };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { accessIssuer, verifyAccessJwt } from './packem_shared/accessIssuer-83KYwaCp.mjs';
|
|
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 n=o=>{const{access:r}=o,t=typeof r=="object"&&r!==null?r.groups:void 0,s=o.groups??t;return Array.isArray(s)?s.filter(e=>typeof e=="string"):void 0};export{n as r};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `next()` advances the middleware chain. Called with no argument it forwards
|
|
3
|
+
* the current context unchanged; called with `{ ctx }` it shallow-merges the
|
|
4
|
+
* extension, and the result type reflects the widened context.
|
|
5
|
+
*/
|
|
6
|
+
interface MiddlewareNext<ContextIn> {
|
|
7
|
+
(): Promise<ContextIn>;
|
|
8
|
+
<Extension extends Record<string, unknown>>(options: {
|
|
9
|
+
ctx: Extension;
|
|
10
|
+
}): Promise<ContextIn & Extension>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A middleware receives the current context and a `next` continuation. Its
|
|
14
|
+
* return type becomes the builder's new context, so `return next({ ctx })`
|
|
15
|
+
* propagates the extension into every downstream `.use()` and the handler.
|
|
16
|
+
*/
|
|
17
|
+
type Middleware<ContextIn, ContextOut> = (options: {
|
|
18
|
+
ctx: ContextIn;
|
|
19
|
+
next: MiddlewareNext<ContextIn>;
|
|
20
|
+
}) => ContextOut | Promise<ContextOut>;
|
|
21
|
+
export { Middleware as M };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `next()` advances the middleware chain. Called with no argument it forwards
|
|
3
|
+
* the current context unchanged; called with `{ ctx }` it shallow-merges the
|
|
4
|
+
* extension, and the result type reflects the widened context.
|
|
5
|
+
*/
|
|
6
|
+
interface MiddlewareNext<ContextIn> {
|
|
7
|
+
(): Promise<ContextIn>;
|
|
8
|
+
<Extension extends Record<string, unknown>>(options: {
|
|
9
|
+
ctx: Extension;
|
|
10
|
+
}): Promise<ContextIn & Extension>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A middleware receives the current context and a `next` continuation. Its
|
|
14
|
+
* return type becomes the builder's new context, so `return next({ ctx })`
|
|
15
|
+
* propagates the extension into every downstream `.use()` and the handler.
|
|
16
|
+
*/
|
|
17
|
+
type Middleware<ContextIn, ContextOut> = (options: {
|
|
18
|
+
ctx: ContextIn;
|
|
19
|
+
next: MiddlewareNext<ContextIn>;
|
|
20
|
+
}) => ContextOut | Promise<ContextOut>;
|
|
21
|
+
export { Middleware as M };
|
|
@@ -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};
|