@lunora/cloudflare-access 1.0.0-alpha.7 → 1.0.0-alpha.71

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/context.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { M as Middleware } from "./packem_shared/index.d-C7lOF4ZA.js";
2
- import { A as AccessClaims } from "./packem_shared/types.d-BO8d74KI.js";
1
+ import { M as Middleware } from "./packem_shared/index.d-D7R8k7yf.js";
2
+ import { A as AccessClaims } from "./packem_shared/types.d-C8c7Qwx1.js";
3
3
  import 'jose';
4
4
  /**
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
- */
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
+ */
11
11
  interface AccessContextInput {
12
12
  auth?: {
13
13
  getIdentity?: () => (Record<string, unknown> | null) | Promise<Record<string, unknown> | null>;
@@ -15,11 +15,11 @@ interface AccessContextInput {
15
15
  };
16
16
  }
17
17
  /**
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
- */
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
+ */
23
23
  interface AccessFacade {
24
24
  /** True when a verified Access identity is present on the request. */
25
25
  readonly authenticated: boolean;
@@ -41,39 +41,39 @@ interface AccessContextOutput extends AccessContextInput {
41
41
  access: AccessFacade;
42
42
  }
43
43
  /**
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
- */
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
+ */
51
51
  declare const accessFacade: (identity: Record<string, unknown> | null | undefined, userId: string | null | undefined) => AccessFacade;
52
52
  /**
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
- */
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
+ */
78
78
  declare const accessContext: <Context extends AccessContextInput>() => Middleware<Context, AccessContextOutput & Context>;
79
79
  export { type AccessContextInput, type AccessContextOutput, type AccessFacade, accessContext, accessFacade };
package/dist/context.mjs CHANGED
@@ -1,36 +1 @@
1
- const stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
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,58 @@
1
- import { a as ResolveIdentityFunction, C as CreateAccessResolverOptions, V as VerifyAccessJwtOptions, A as AccessClaims } from "./packem_shared/types.d-BO8d74KI.mjs";
2
- export type { b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-BO8d74KI.mjs";
1
+ import { a as ResolveIdentityFunction, C as CreateAccessResolverOptions, V as VerifyAccessJwtOptions, A as AccessClaims } from "./packem_shared/types.d-C8c7Qwx1.mjs";
2
+ export type { b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-C8c7Qwx1.mjs";
3
3
  import 'jose';
4
4
  /**
5
- * Create a `resolveIdentity` adapter for Cloudflare Access. The returned
6
- * function reads the Access JWT off the request, verifies it (`verifyAccessJwt`),
7
- * and maps the claims onto the identity shape `@lunora/runtime` expects — so a
8
- * verified Access user/service-token becomes `ctx.auth` for every
9
- * query/mutation/action (and feeds RLS) with no further wiring.
10
- *
11
- * Behaviour is **fail-closed → anonymous**: a missing token, or a token that
12
- * fails verification, resolves to `null` (the request proceeds unauthenticated
13
- * and RLS denies). Use {@link CreateAccessResolverOptions.onError} to observe
14
- * verification failures.
15
- *
16
- * Wire it in your worker entry:
17
- *
18
- * ```ts
19
- * options.resolveIdentity = createAccessResolver({
20
- * teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
21
- * aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
22
- * });
23
- * ```
24
- */
5
+ * Create a `resolveIdentity` adapter for Cloudflare Access. The returned
6
+ * function reads the Access JWT off the request, verifies it (`verifyAccessJwt`),
7
+ * and maps the claims onto the identity shape `@lunora/runtime` expects — so a
8
+ * verified Access user/service-token becomes `ctx.auth` for every
9
+ * query/mutation/action (and feeds RLS) with no further wiring.
10
+ *
11
+ * Behaviour is **fail-closed → anonymous**: a missing token, or a token that
12
+ * fails verification, resolves to `null` (the request proceeds unauthenticated
13
+ * and RLS denies). Use {@link CreateAccessResolverOptions.onError} to observe
14
+ * verification failures.
15
+ *
16
+ * Wire it in your worker entry:
17
+ *
18
+ * ```ts
19
+ * options.resolveIdentity = createAccessResolver({
20
+ * teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
21
+ * aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
22
+ * });
23
+ * ```
24
+ */
25
25
  declare const createAccessResolver: (options: CreateAccessResolverOptions) => ResolveIdentityFunction;
26
26
  /**
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
- */
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
+ */
34
34
  declare const composeResolvers: (...resolvers: ResolveIdentityFunction[]) => ResolveIdentityFunction;
35
35
  /**
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
- */
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
+ */
42
42
  declare const accessIssuer: (teamDomain: string) => string;
43
43
  /**
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
- */
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
+ */
57
57
  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
58
  export { type AccessClaims, type CreateAccessResolverOptions, type ResolveIdentityFunction, type VerifyAccessJwtOptions, accessIssuer, composeResolvers, createAccessResolver, verifyAccessJwt };
package/dist/index.d.ts CHANGED
@@ -1,71 +1,58 @@
1
- import { a as ResolveIdentityFunction, C as CreateAccessResolverOptions, V as VerifyAccessJwtOptions, A as AccessClaims } from "./packem_shared/types.d-BO8d74KI.js";
2
- export type { b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-BO8d74KI.js";
1
+ import { a as ResolveIdentityFunction, C as CreateAccessResolverOptions, V as VerifyAccessJwtOptions, A as AccessClaims } from "./packem_shared/types.d-C8c7Qwx1.js";
2
+ export type { b as AccessKeySet, c as ResolvedAccessIdentity, d as ResolvedIdentityLike } from "./packem_shared/types.d-C8c7Qwx1.js";
3
3
  import 'jose';
4
4
  /**
5
- * Create a `resolveIdentity` adapter for Cloudflare Access. The returned
6
- * function reads the Access JWT off the request, verifies it (`verifyAccessJwt`),
7
- * and maps the claims onto the identity shape `@lunora/runtime` expects — so a
8
- * verified Access user/service-token becomes `ctx.auth` for every
9
- * query/mutation/action (and feeds RLS) with no further wiring.
10
- *
11
- * Behaviour is **fail-closed → anonymous**: a missing token, or a token that
12
- * fails verification, resolves to `null` (the request proceeds unauthenticated
13
- * and RLS denies). Use {@link CreateAccessResolverOptions.onError} to observe
14
- * verification failures.
15
- *
16
- * Wire it in your worker entry:
17
- *
18
- * ```ts
19
- * options.resolveIdentity = createAccessResolver({
20
- * teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
21
- * aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
22
- * });
23
- * ```
24
- */
5
+ * Create a `resolveIdentity` adapter for Cloudflare Access. The returned
6
+ * function reads the Access JWT off the request, verifies it (`verifyAccessJwt`),
7
+ * and maps the claims onto the identity shape `@lunora/runtime` expects — so a
8
+ * verified Access user/service-token becomes `ctx.auth` for every
9
+ * query/mutation/action (and feeds RLS) with no further wiring.
10
+ *
11
+ * Behaviour is **fail-closed → anonymous**: a missing token, or a token that
12
+ * fails verification, resolves to `null` (the request proceeds unauthenticated
13
+ * and RLS denies). Use {@link CreateAccessResolverOptions.onError} to observe
14
+ * verification failures.
15
+ *
16
+ * Wire it in your worker entry:
17
+ *
18
+ * ```ts
19
+ * options.resolveIdentity = createAccessResolver({
20
+ * teamDomain: env.CF_ACCESS_TEAM_DOMAIN, // "acme" | "acme.cloudflareaccess.com"
21
+ * aud: env.CF_ACCESS_AUD, // the Access app's AUD tag
22
+ * });
23
+ * ```
24
+ */
25
25
  declare const createAccessResolver: (options: CreateAccessResolverOptions) => ResolveIdentityFunction;
26
26
  /**
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
- */
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
+ */
34
34
  declare const composeResolvers: (...resolvers: ResolveIdentityFunction[]) => ResolveIdentityFunction;
35
35
  /**
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
- */
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
+ */
42
42
  declare const accessIssuer: (teamDomain: string) => string;
43
43
  /**
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
- */
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
+ */
57
57
  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
58
  export { type AccessClaims, type CreateAccessResolverOptions, type ResolveIdentityFunction, type VerifyAccessJwtOptions, accessIssuer, composeResolvers, createAccessResolver, verifyAccessJwt };
package/dist/index.mjs CHANGED
@@ -1,2 +1 @@
1
- export { composeResolvers, createAccessResolver } from './packem_shared/composeResolvers-C_7T5dDj.mjs';
2
- export { accessIssuer, verifyAccessJwt } from './packem_shared/accessIssuer-DhKaNoyU.mjs';
1
+ import{composeResolvers as r,createAccessResolver as c}from"./packem_shared/composeResolvers--vZvblPO.mjs";import{accessIssuer as t,verifyAccessJwt as f}from"./packem_shared/accessIssuer-B6vVJPus.mjs";export{t as accessIssuer,r as composeResolvers,c as createAccessResolver,f as verifyAccessJwt};
@@ -0,0 +1 @@
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};
@@ -0,0 +1 @@
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};
@@ -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,134 @@
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 };