@lunora/cloudflare-access 1.0.0-alpha.29 → 1.0.0-alpha.30
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.mjs +1 -11
- package/dist/context.mjs +1 -37
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/accessIssuer-BdF2LQuC.mjs +1 -0
- package/dist/packem_shared/composeResolvers-BZQFvmWK.mjs +1 -0
- package/dist/packem_shared/identity-groups-lzvxGu3k.mjs +1 -0
- package/dist/roles.mjs +1 -31
- package/package.json +1 -1
- package/dist/packem_shared/accessIssuer-C69kkiYc.mjs +0 -99
- package/dist/packem_shared/composeResolvers-BlVswT75.mjs +0 -39
- package/dist/packem_shared/identity-groups-CONuLY61.mjs +0 -8
package/dist/admin.mjs
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const accessAdminGate = (options) => {
|
|
4
|
-
assertVerifyOptions(options);
|
|
5
|
-
return async (request) => {
|
|
6
|
-
const claims = await verifyRequest(request, options);
|
|
7
|
-
return claims === void 0 ? false : options.isAdmin(claims);
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export { accessAdminGate };
|
|
1
|
+
import{assertVerifyOptions as i,verifyRequest as e}from"./packem_shared/accessIssuer-BdF2LQuC.mjs";const r=s=>(i(s),async t=>{const a=await e(t,s);return a===void 0?!1:s.isAdmin(a)});export{r as accessAdminGate};
|
package/dist/context.mjs
CHANGED
|
@@ -1,37 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const stringClaim = (value) => typeof value === "string" ? value : void 0;
|
|
4
|
-
const ANONYMOUS_FACADE = {
|
|
5
|
-
authenticated: false,
|
|
6
|
-
claims: void 0,
|
|
7
|
-
commonName: void 0,
|
|
8
|
-
email: void 0,
|
|
9
|
-
groups: [],
|
|
10
|
-
hasGroup: () => false,
|
|
11
|
-
userId: void 0
|
|
12
|
-
};
|
|
13
|
-
const facadeFor = (identity, userId) => {
|
|
14
|
-
const raw = identity["access"];
|
|
15
|
-
if (typeof raw !== "object" || raw === null) {
|
|
16
|
-
return ANONYMOUS_FACADE;
|
|
17
|
-
}
|
|
18
|
-
const claims = raw;
|
|
19
|
-
const groups = readIdentityGroups(identity) ?? [];
|
|
20
|
-
return {
|
|
21
|
-
authenticated: true,
|
|
22
|
-
claims,
|
|
23
|
-
commonName: stringClaim(identity["commonName"]) ?? stringClaim(claims.common_name),
|
|
24
|
-
email: stringClaim(identity["email"]) ?? stringClaim(claims.email),
|
|
25
|
-
groups,
|
|
26
|
-
hasGroup: (group) => groups.includes(group),
|
|
27
|
-
userId
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
const accessFacade = (identity, userId) => identity ? facadeFor(identity, userId ?? void 0) : ANONYMOUS_FACADE;
|
|
31
|
-
const accessContext = () => async ({ ctx, next }) => {
|
|
32
|
-
const identity = await ctx.auth?.getIdentity?.();
|
|
33
|
-
const access = accessFacade(identity, ctx.auth?.userId);
|
|
34
|
-
return next({ ctx: { access } });
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export { accessContext, accessFacade };
|
|
1
|
+
import{t as m}from"./packem_shared/identity-groups-lzvxGu3k.mjs";const e=t=>typeof t=="string"?t:void 0,i={authenticated:!1,claims:void 0,commonName:void 0,email:void 0,groups:[],hasGroup:()=>!1,userId:void 0},r=(t,a)=>{const o=t.access;if(typeof o!="object"||o===null)return i;const s=o,c=m(t)??[];return{authenticated:!0,claims:s,commonName:e(t.commonName)??e(s.common_name),email:e(t.email)??e(s.email),groups:c,hasGroup:n=>c.includes(n),userId:a}},u=(t,a)=>t?r(t,a??void 0):i,l=()=>async({ctx:t,next:a})=>{const o=await t.auth?.getIdentity?.(),s=u(o,t.auth?.userId);return a({ctx:{access:s}})};export{l as accessContext,u as accessFacade};
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { accessIssuer, verifyAccessJwt } from './packem_shared/accessIssuer-C69kkiYc.mjs';
|
|
1
|
+
import{composeResolvers as r,createAccessResolver as c}from"./packem_shared/composeResolvers-BZQFvmWK.mjs";import{accessIssuer as t,verifyAccessJwt as f}from"./packem_shared/accessIssuer-BdF2LQuC.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 d}from"jose";const h="cf-access-jwt-assertion",p="CF_Authorization",g=(t,e,r)=>{const o=t.headers.get(e);if(o!==null&&o.length>0)return o;const s=t.headers.get("cookie");if(s!==null)for(const c of s.split(";")){const a=c.indexOf("=");if(a!==-1&&c.slice(0,a).trim()===r){const n=c.slice(a+1).trim();return n.length>0?n:void 0}}},w="/cdn-cgi/access/certs",y=/^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(y,""));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,R=t=>{let e=i.get(t);return e===void 0&&(e=d(new URL(`${t}${w}`)),i.set(t,e)),e},f=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},N=t=>{u(t.teamDomain),f(t.aud)},L=async(t,e)=>{const r=u(e.teamDomain),o=f(e.aud),s=e.keySet??R(r),{payload:c}=await m(t,s,{algorithms:["RS256"],audience:o,clockTolerance:e.clockToleranceSec,issuer:r});return c},$=async(t,e)=>{const r=(e.headerName??h).toLowerCase(),o=e.cookieName??p,s=g(t,r,o);if(s!==void 0)try{return await L(s,e)}catch(c){try{e.onError?.(c,t)}catch{}return}};export{u as accessIssuer,N as assertVerifyOptions,L as verifyAccessJwt,$ as verifyRequest};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{assertVerifyOptions as t,verifyRequest as c}from"./accessIssuer-BdF2LQuC.mjs";const i=null,a=o=>typeof o=="string"&&o.length>0?o:void 0,e=o=>a(o.sub)??a(o.email)??a(o.common_name),u=(o,n)=>{const s=n?.(o)??{},r=a(s.userId)??e(o);return r===void 0?i:{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},...s,userId:r}},l=o=>(t(o),async n=>{const s=await c(n,o);return s===void 0?i:u(s,o.mapClaims)}),v=(...o)=>async(n,s)=>{for(const r of o){const m=await r(n,s);if(m)return m}return i};export{v as composeResolvers,l as createAccessResolver};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=r=>{const{access:o}=r,s=typeof o=="object"&&o!==null?o.groups:void 0,t=r.groups??s;return Array.isArray(t)?t.filter(e=>typeof e=="string"):void 0};export{n as t};
|
package/dist/roles.mjs
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const rolesForGroup = (group, map) => {
|
|
4
|
-
if (map === void 0) {
|
|
5
|
-
return [group];
|
|
6
|
-
}
|
|
7
|
-
const resolved = typeof map === "function" ? map(group) : map[group];
|
|
8
|
-
if (resolved === void 0) {
|
|
9
|
-
return [];
|
|
10
|
-
}
|
|
11
|
-
return Array.isArray(resolved) ? resolved : [resolved];
|
|
12
|
-
};
|
|
13
|
-
const accessRoles = (options = {}) => {
|
|
14
|
-
const readGroups = options.readGroups ?? readIdentityGroups;
|
|
15
|
-
return async ({ ctx, next }) => {
|
|
16
|
-
const identity = await ctx.auth?.getIdentity?.() ?? void 0;
|
|
17
|
-
const groups = identity ? readGroups(identity) : void 0;
|
|
18
|
-
if (groups === void 0 || groups.length === 0) {
|
|
19
|
-
return next();
|
|
20
|
-
}
|
|
21
|
-
const roles = new Set(ctx.auth?.roles);
|
|
22
|
-
for (const group of groups) {
|
|
23
|
-
for (const role of rolesForGroup(group, options.map)) {
|
|
24
|
-
roles.add(role);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return next({ ctx: { auth: { ...ctx.auth, roles: [...roles] } } });
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export { accessRoles };
|
|
1
|
+
import{t as u}from"./packem_shared/identity-groups-lzvxGu3k.mjs";const d=(o,r)=>{if(r===void 0)return[o];const t=typeof r=="function"?r(o):r[o];return t===void 0?[]:Array.isArray(t)?t:[t]},h=(o={})=>{const r=o.readGroups??u;return async({ctx:t,next:n})=>{const a=await t.auth?.getIdentity?.()??void 0,e=a?r(a):void 0;if(e===void 0||e.length===0)return n();const s=new Set(t.auth?.roles);for(const c of e)for(const i of d(c,o.map))s.add(i);return n({ctx:{auth:{...t.auth,roles:[...s]}}})}};export{h as accessRoles};
|
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.30",
|
|
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",
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_HEADER = "cf-access-jwt-assertion";
|
|
5
|
-
const DEFAULT_COOKIE = "CF_Authorization";
|
|
6
|
-
const readToken = (request, headerName, cookieName) => {
|
|
7
|
-
const headerValue = request.headers.get(headerName);
|
|
8
|
-
if (headerValue !== null && headerValue.length > 0) {
|
|
9
|
-
return headerValue;
|
|
10
|
-
}
|
|
11
|
-
const cookieHeader = request.headers.get("cookie");
|
|
12
|
-
if (cookieHeader === null) {
|
|
13
|
-
return void 0;
|
|
14
|
-
}
|
|
15
|
-
for (const part of cookieHeader.split(";")) {
|
|
16
|
-
const eq = part.indexOf("=");
|
|
17
|
-
if (eq === -1) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (part.slice(0, eq).trim() === cookieName) {
|
|
21
|
-
const value = part.slice(eq + 1).trim();
|
|
22
|
-
return value.length > 0 ? value : void 0;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return void 0;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const CERTS_PATH = "/cdn-cgi/access/certs";
|
|
29
|
-
const SCHEME_PREFIX = /^https?:\/\//i;
|
|
30
|
-
const stripTrailingSlashes = (value) => {
|
|
31
|
-
let end = value.length;
|
|
32
|
-
while (end > 0 && value[end - 1] === "/") {
|
|
33
|
-
end -= 1;
|
|
34
|
-
}
|
|
35
|
-
return value.slice(0, end);
|
|
36
|
-
};
|
|
37
|
-
const accessIssuer = (teamDomain) => {
|
|
38
|
-
const trimmed = stripTrailingSlashes(teamDomain.trim().replace(SCHEME_PREFIX, ""));
|
|
39
|
-
if (trimmed.length === 0) {
|
|
40
|
-
throw new LunoraError("INTERNAL", '@lunora/cloudflare-access: `teamDomain` is required (e.g. "acme" or "acme.cloudflareaccess.com")');
|
|
41
|
-
}
|
|
42
|
-
const candidate = trimmed.includes(".") ? `https://${trimmed}` : `https://${trimmed}.cloudflareaccess.com`;
|
|
43
|
-
const host = new URL(candidate).host.toLowerCase();
|
|
44
|
-
return `https://${host}`;
|
|
45
|
-
};
|
|
46
|
-
const jwksByIssuer = /* @__PURE__ */ new Map();
|
|
47
|
-
const remoteJwks = (issuer) => {
|
|
48
|
-
let getter = jwksByIssuer.get(issuer);
|
|
49
|
-
if (getter === void 0) {
|
|
50
|
-
getter = createRemoteJWKSet(new URL(`${issuer}${CERTS_PATH}`));
|
|
51
|
-
jwksByIssuer.set(issuer, getter);
|
|
52
|
-
}
|
|
53
|
-
return getter;
|
|
54
|
-
};
|
|
55
|
-
const normalizeAudiences = (aud) => {
|
|
56
|
-
const audiences = (Array.isArray(aud) ? aud : [aud]).filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
57
|
-
if (audiences.length === 0) {
|
|
58
|
-
throw new LunoraError(
|
|
59
|
-
"INTERNAL",
|
|
60
|
-
"@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"
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
return audiences;
|
|
64
|
-
};
|
|
65
|
-
const assertVerifyOptions = (options) => {
|
|
66
|
-
accessIssuer(options.teamDomain);
|
|
67
|
-
normalizeAudiences(options.aud);
|
|
68
|
-
};
|
|
69
|
-
const verifyAccessJwt = async (token, options) => {
|
|
70
|
-
const issuer = accessIssuer(options.teamDomain);
|
|
71
|
-
const audiences = normalizeAudiences(options.aud);
|
|
72
|
-
const keySet = options.keySet ?? remoteJwks(issuer);
|
|
73
|
-
const { payload } = await jwtVerify(token, keySet, {
|
|
74
|
-
algorithms: ["RS256"],
|
|
75
|
-
audience: audiences,
|
|
76
|
-
clockTolerance: options.clockToleranceSec,
|
|
77
|
-
issuer
|
|
78
|
-
});
|
|
79
|
-
return payload;
|
|
80
|
-
};
|
|
81
|
-
const verifyRequest = async (request, options) => {
|
|
82
|
-
const headerName = (options.headerName ?? DEFAULT_HEADER).toLowerCase();
|
|
83
|
-
const cookieName = options.cookieName ?? DEFAULT_COOKIE;
|
|
84
|
-
const token = readToken(request, headerName, cookieName);
|
|
85
|
-
if (token === void 0) {
|
|
86
|
-
return void 0;
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
return await verifyAccessJwt(token, options);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
try {
|
|
92
|
-
options.onError?.(error, request);
|
|
93
|
-
} catch {
|
|
94
|
-
}
|
|
95
|
-
return void 0;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export { accessIssuer, assertVerifyOptions, verifyAccessJwt, verifyRequest };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { assertVerifyOptions, verifyRequest } from './accessIssuer-C69kkiYc.mjs';
|
|
2
|
-
|
|
3
|
-
const ANONYMOUS = null;
|
|
4
|
-
const nonEmpty = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
5
|
-
const deriveUserId = (claims) => nonEmpty(claims.sub) ?? nonEmpty(claims.email) ?? nonEmpty(claims.common_name);
|
|
6
|
-
const toIdentity = (claims, mapClaims) => {
|
|
7
|
-
const overrides = mapClaims?.(claims) ?? {};
|
|
8
|
-
const userId = nonEmpty(overrides.userId) ?? deriveUserId(claims);
|
|
9
|
-
if (userId === void 0) {
|
|
10
|
-
return ANONYMOUS;
|
|
11
|
-
}
|
|
12
|
-
return {
|
|
13
|
-
access: claims,
|
|
14
|
-
...claims.common_name === void 0 ? {} : { commonName: claims.common_name },
|
|
15
|
-
...claims.email === void 0 ? {} : { email: claims.email },
|
|
16
|
-
...claims.exp === void 0 ? {} : { exp: claims.exp },
|
|
17
|
-
...claims.groups === void 0 ? {} : { groups: claims.groups },
|
|
18
|
-
...overrides,
|
|
19
|
-
userId
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
const createAccessResolver = (options) => {
|
|
23
|
-
assertVerifyOptions(options);
|
|
24
|
-
return async (request) => {
|
|
25
|
-
const claims = await verifyRequest(request, options);
|
|
26
|
-
return claims === void 0 ? ANONYMOUS : toIdentity(claims, options.mapClaims);
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
const composeResolvers = (...resolvers) => async (request, env) => {
|
|
30
|
-
for (const resolve of resolvers) {
|
|
31
|
-
const identity = await resolve(request, env);
|
|
32
|
-
if (identity) {
|
|
33
|
-
return identity;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return ANONYMOUS;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export { composeResolvers, createAccessResolver };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
const readIdentityGroups = (identity) => {
|
|
2
|
-
const { access } = identity;
|
|
3
|
-
const nested = typeof access === "object" && access !== null ? access.groups : void 0;
|
|
4
|
-
const groups = identity["groups"] ?? nested;
|
|
5
|
-
return Array.isArray(groups) ? groups.filter((group) => typeof group === "string") : void 0;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export { readIdentityGroups as r };
|