@wizzlethorpe/vaults 0.10.0 → 0.12.0
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/README.md +17 -3
- package/dist/build.js +37 -1
- package/dist/build.js.map +1 -1
- package/dist/commands/oidc.js +242 -0
- package/dist/commands/oidc.js.map +1 -0
- package/dist/commands/preview.js +4 -0
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/push.js +3 -0
- package/dist/commands/push.js.map +1 -1
- package/dist/config.js +21 -1
- package/dist/config.js.map +1 -1
- package/dist/foundry-importer.bundle.js +8 -5
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -1
- package/dist/render/auth-template.js +199 -12
- package/dist/render/auth-template.js.map +1 -1
- package/dist/render/handlers/builtin/battlemap.js +30 -3
- package/dist/render/handlers/builtin/battlemap.js.map +1 -1
- package/dist/render/oidc-match.js +30 -0
- package/dist/render/oidc-match.js.map +1 -0
- package/package.json +1 -1
|
@@ -6,15 +6,18 @@ export function renderAuthMiddleware(cfg) {
|
|
|
6
6
|
const rolesLiteral = JSON.stringify(cfg.roles);
|
|
7
7
|
const passwordsLiteral = JSON.stringify(cfg.rolePasswords);
|
|
8
8
|
const patreonLiteral = JSON.stringify(cfg.patreon ?? null);
|
|
9
|
+
const oidcLiteral = JSON.stringify(cfg.oidc ?? null);
|
|
9
10
|
return `// Auto-generated by the vaults CLI. Do not edit by hand.
|
|
10
11
|
// Roles, password hashes, and routing live here so the deployed Function
|
|
11
12
|
// is fully self-contained and doesn't need any other binding besides
|
|
12
13
|
// SESSION_SECRET (set via wrangler secret). When Patreon is configured,
|
|
13
|
-
// PATREON_CLIENT_SECRET also has to be set as a Wrangler secret
|
|
14
|
+
// PATREON_CLIENT_SECRET also has to be set as a Wrangler secret; when
|
|
15
|
+
// OIDC is configured, OAUTH_CLIENT_SECRET likewise.
|
|
14
16
|
|
|
15
17
|
const ROLES = ${rolesLiteral};
|
|
16
18
|
const PASSWORDS = ${passwordsLiteral};
|
|
17
19
|
const PATREON = ${patreonLiteral};
|
|
20
|
+
const OIDC = ${oidcLiteral};
|
|
18
21
|
const COOKIE_NAME = "vault_role";
|
|
19
22
|
// Non-HttpOnly companion cookie carrying the role name only; the auth check
|
|
20
23
|
// uses COOKIE_NAME (which is signed and HttpOnly), this one is purely for UI.
|
|
@@ -97,6 +100,17 @@ export const onRequest = async (ctx) => {
|
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
// /auth/oidc/* — only mounted when the build saw an OIDC config with at
|
|
104
|
+
// least one role rule. Same session-cookie contract as password login.
|
|
105
|
+
if (OIDC) {
|
|
106
|
+
if (url.pathname === "/auth/oidc/start" && request.method === "GET") {
|
|
107
|
+
return handleOidcStart(request, env);
|
|
108
|
+
}
|
|
109
|
+
if (url.pathname === "/auth/oidc/callback" && request.method === "GET") {
|
|
110
|
+
return handleOidcCallback(request, env);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
// /_batch; bulk source fetch for sync clients (Foundry). Body is
|
|
101
115
|
// newline-separated paths under text/plain so the request stays CORS-
|
|
102
116
|
// simple (no preflight per file → no OPTIONS rate-limit). Response is
|
|
@@ -500,8 +514,11 @@ function escAttr(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&"
|
|
|
500
514
|
// /login with an error param on failure. CSRF state rides through a short-
|
|
501
515
|
// lived signed cookie so the callback can verify the round-trip.
|
|
502
516
|
|
|
503
|
-
|
|
504
|
-
|
|
517
|
+
// Shared by the Patreon and OIDC flows; only one OAuth round-trip can be in
|
|
518
|
+
// flight per browser at a time, and the state UUID check keeps a callback
|
|
519
|
+
// from one flow from completing the other's.
|
|
520
|
+
const STATE_COOKIE = "vault_oauth_state";
|
|
521
|
+
const STATE_TTL = 600; // 10 minutes — long enough for the user to authorise
|
|
505
522
|
|
|
506
523
|
async function handlePatreonStart(request, env) {
|
|
507
524
|
if (!env.PATREON_CLIENT_SECRET) {
|
|
@@ -538,7 +555,7 @@ async function handlePatreonCallback(request, env) {
|
|
|
538
555
|
|
|
539
556
|
const stateData = await readStateCookie(request, env.SESSION_SECRET);
|
|
540
557
|
// Always clear the state cookie regardless of outcome.
|
|
541
|
-
const clearState = clearCookieVariants(
|
|
558
|
+
const clearState = clearCookieVariants(STATE_COOKIE);
|
|
542
559
|
|
|
543
560
|
if (!stateData || stateData.state !== state) {
|
|
544
561
|
const headers = new Headers({ Location: "/login?error=patreon_state_mismatch" });
|
|
@@ -648,23 +665,168 @@ async function patreonFetchIdentity(accessToken) {
|
|
|
648
665
|
return await res.json();
|
|
649
666
|
}
|
|
650
667
|
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
668
|
+
// ── OIDC OAuth (optional, additive to password login) ────────────────────
|
|
669
|
+
// Bound only when OIDC !== null (i.e. the build saw an oidc block with at
|
|
670
|
+
// least one role rule). Standard authorization-code flow with PKCE (S256):
|
|
671
|
+
// the code_verifier rides in the signed state cookie, the challenge goes to
|
|
672
|
+
// the authorization endpoint. Identity comes from the userinfo endpoint;
|
|
673
|
+
// the visitor's email is matched against per-role email/domain rules.
|
|
674
|
+
|
|
675
|
+
async function handleOidcStart(request, env) {
|
|
676
|
+
if (!env.OAUTH_CLIENT_SECRET) {
|
|
677
|
+
return new Response("OIDC login is misconfigured: OAUTH_CLIENT_SECRET secret is missing.", { status: 500 });
|
|
678
|
+
}
|
|
679
|
+
const url = new URL(request.url);
|
|
680
|
+
const next = safeNext(url.searchParams.get("next"));
|
|
681
|
+
const stateValue = crypto.randomUUID();
|
|
682
|
+
const verifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
683
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
684
|
+
const challenge = base64UrlEncode(new Uint8Array(digest));
|
|
685
|
+
const stateCookie = await signStateCookie({ state: stateValue, next, verifier }, env.SESSION_SECRET);
|
|
686
|
+
|
|
687
|
+
const authorize = new URL(OIDC.authorizationEndpoint);
|
|
688
|
+
authorize.searchParams.set("response_type", "code");
|
|
689
|
+
authorize.searchParams.set("client_id", OIDC.clientId);
|
|
690
|
+
authorize.searchParams.set("redirect_uri", url.origin + "/auth/oidc/callback");
|
|
691
|
+
authorize.searchParams.set("scope", "openid email");
|
|
692
|
+
authorize.searchParams.set("state", stateValue);
|
|
693
|
+
authorize.searchParams.set("code_challenge", challenge);
|
|
694
|
+
authorize.searchParams.set("code_challenge_method", "S256");
|
|
695
|
+
|
|
696
|
+
const headers = new Headers({ Location: authorize.toString() });
|
|
697
|
+
headers.append("Set-Cookie", stateCookie);
|
|
698
|
+
return new Response(null, { status: 302, headers });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function handleOidcCallback(request, env) {
|
|
702
|
+
if (!env.OAUTH_CLIENT_SECRET) {
|
|
703
|
+
return loginRedirect("/", "oidc_misconfigured");
|
|
704
|
+
}
|
|
705
|
+
const url = new URL(request.url);
|
|
706
|
+
const code = url.searchParams.get("code");
|
|
707
|
+
const state = url.searchParams.get("state");
|
|
708
|
+
if (!code || !state) return loginRedirect("/", "oidc_failed");
|
|
709
|
+
|
|
710
|
+
const stateData = await readStateCookie(request, env.SESSION_SECRET);
|
|
711
|
+
// Always clear the state cookie regardless of outcome.
|
|
712
|
+
const clearState = clearCookieVariants(STATE_COOKIE);
|
|
713
|
+
|
|
714
|
+
if (!stateData || stateData.state !== state) {
|
|
715
|
+
const headers = new Headers({ Location: "/login?error=oidc_state_mismatch" });
|
|
716
|
+
for (const v of clearState) headers.append("Set-Cookie", v);
|
|
717
|
+
return new Response(null, { status: 302, headers });
|
|
718
|
+
}
|
|
719
|
+
const next = stateData.next || "/";
|
|
720
|
+
|
|
721
|
+
// Exchange code → access token; used once, immediately, for userinfo.
|
|
722
|
+
let access;
|
|
723
|
+
try {
|
|
724
|
+
access = await oidcExchangeCode(code, url.origin + "/auth/oidc/callback", env.OAUTH_CLIENT_SECRET, stateData.verifier);
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.warn("OIDC token exchange failed:", err);
|
|
727
|
+
const headers = new Headers({ Location: "/login?error=oidc_token_exchange" });
|
|
728
|
+
for (const v of clearState) headers.append("Set-Cookie", v);
|
|
729
|
+
return new Response(null, { status: 302, headers });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let email;
|
|
733
|
+
try { email = await oidcFetchEmail(access); }
|
|
734
|
+
catch (err) {
|
|
735
|
+
console.warn("OIDC userinfo fetch failed:", err);
|
|
736
|
+
const headers = new Headers({ Location: "/login?error=oidc_identity" });
|
|
737
|
+
for (const v of clearState) headers.append("Set-Cookie", v);
|
|
738
|
+
return new Response(null, { status: 302, headers });
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const role = matchOidcRole(email, OIDC.roleRules);
|
|
742
|
+
if (!role) {
|
|
743
|
+
// Authenticated, but no rule grants this email a role.
|
|
744
|
+
const headers = new Headers({ Location: "/login?error=oidc_no_role&next=" + encodeURIComponent(next) });
|
|
745
|
+
for (const v of clearState) headers.append("Set-Cookie", v);
|
|
746
|
+
return new Response(null, { status: 302, headers });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const cookie = await signSessionCookie(role, env.SESSION_SECRET);
|
|
750
|
+
const headers = new Headers({ Location: next });
|
|
751
|
+
headers.append("Set-Cookie", cookie);
|
|
752
|
+
headers.append("Set-Cookie", DISPLAY_COOKIE_NAME + "=" + encodeURIComponent(role)
|
|
753
|
+
+ "; Path=/; Secure; SameSite=None; Partitioned; Max-Age=" + COOKIE_MAX_AGE);
|
|
754
|
+
for (const v of clearState) headers.append("Set-Cookie", v);
|
|
755
|
+
return new Response(null, { status: 302, headers });
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Duplicated from cli/src/render/oidc-match.ts — the worker can't import TS
|
|
759
|
+
// modules, so keep the two copies in sync. Matching is exact and case-
|
|
760
|
+
// insensitive; a domain rule matches only the exact domain after the last
|
|
761
|
+
// '@' (subdomains are not implied).
|
|
762
|
+
function matchOidcRole(email, roleRules) {
|
|
763
|
+
const at = email.lastIndexOf("@");
|
|
764
|
+
if (at < 0) return null;
|
|
765
|
+
const lower = email.toLowerCase();
|
|
766
|
+
const domain = lower.slice(at + 1);
|
|
767
|
+
for (let i = ROLES.length - 1; i >= 0; i--) {
|
|
768
|
+
const rule = roleRules[ROLES[i]];
|
|
769
|
+
if (!rule) continue;
|
|
770
|
+
if ((rule.emails || []).some((e) => e.toLowerCase() === lower)) return ROLES[i];
|
|
771
|
+
if (domain && (rule.domains || []).some((d) => d.toLowerCase() === domain)) return ROLES[i];
|
|
772
|
+
}
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function oidcExchangeCode(code, redirectUri, clientSecret, verifier) {
|
|
777
|
+
const body = new URLSearchParams({
|
|
778
|
+
grant_type: "authorization_code",
|
|
779
|
+
code,
|
|
780
|
+
client_id: OIDC.clientId,
|
|
781
|
+
client_secret: clientSecret,
|
|
782
|
+
redirect_uri: redirectUri,
|
|
783
|
+
code_verifier: verifier || "",
|
|
784
|
+
});
|
|
785
|
+
const res = await fetch(OIDC.tokenEndpoint, {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
788
|
+
body: body.toString(),
|
|
789
|
+
});
|
|
790
|
+
if (!res.ok) throw new Error("token exchange status " + res.status);
|
|
791
|
+
const data = await res.json();
|
|
792
|
+
if (!data.access_token) throw new Error("no access_token in response");
|
|
793
|
+
return data.access_token;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function oidcFetchEmail(accessToken) {
|
|
797
|
+
const res = await fetch(OIDC.userinfoEndpoint, {
|
|
798
|
+
headers: { Authorization: "Bearer " + accessToken },
|
|
799
|
+
});
|
|
800
|
+
if (!res.ok) throw new Error("userinfo status " + res.status);
|
|
801
|
+
const data = await res.json();
|
|
802
|
+
if (typeof data.email !== "string" || !data.email) throw new Error("userinfo returned no email");
|
|
803
|
+
return data.email;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function base64UrlEncode(bytes) {
|
|
807
|
+
let s = "";
|
|
808
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
809
|
+
return btoa(s).replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=+$/, "");
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// State cookie: stores { state, next } (plus the PKCE verifier for OIDC)
|
|
813
|
+
// signed with SESSION_SECRET, used to verify the OAuth callback came from
|
|
814
|
+
// our /start handler and not a forgery. Shared by both OAuth flows.
|
|
815
|
+
// Lifetime is short (STATE_TTL); kept distinct from the long-lived
|
|
654
816
|
// session cookie so a stolen state cookie can't impersonate a session.
|
|
655
817
|
|
|
656
818
|
async function signStateCookie(payload, secret) {
|
|
657
|
-
const exp = Math.floor(Date.now() / 1000) +
|
|
819
|
+
const exp = Math.floor(Date.now() / 1000) + STATE_TTL;
|
|
658
820
|
const data = JSON.stringify({ ...payload, exp });
|
|
659
821
|
const sig = await hmac(data, secret);
|
|
660
822
|
const value = btoa(data) + "." + sig;
|
|
661
|
-
return
|
|
662
|
-
+ "; Path=/; Secure; SameSite=Lax; HttpOnly; Max-Age=" +
|
|
823
|
+
return STATE_COOKIE + "=" + value
|
|
824
|
+
+ "; Path=/; Secure; SameSite=Lax; HttpOnly; Max-Age=" + STATE_TTL;
|
|
663
825
|
}
|
|
664
826
|
|
|
665
827
|
async function readStateCookie(request, secret) {
|
|
666
828
|
const cookies = parseCookie(request.headers.get("Cookie") || "");
|
|
667
|
-
const raw = cookies[
|
|
829
|
+
const raw = cookies[STATE_COOKIE];
|
|
668
830
|
if (!raw) return null;
|
|
669
831
|
const dot = raw.lastIndexOf(".");
|
|
670
832
|
if (dot < 0) return null;
|
|
@@ -890,10 +1052,17 @@ export const LOGIN_HTML = `<!doctype html>
|
|
|
890
1052
|
}
|
|
891
1053
|
.patreon-btn:hover { filter: brightness(1.05); }
|
|
892
1054
|
.patreon-hint { color: var(--muted); font-size: 0.78rem; margin-top: 0.6rem; text-align: center; }
|
|
1055
|
+
.oidc-btn {
|
|
1056
|
+
display: flex; align-items: center; justify-content: center;
|
|
1057
|
+
width: 100%; padding: 0.55rem 1rem; font: inherit; font-size: 0.95rem;
|
|
1058
|
+
background: var(--accent); color: var(--accent-fg); border: 0; border-radius: 4px;
|
|
1059
|
+
cursor: pointer; text-decoration: none;
|
|
1060
|
+
}
|
|
1061
|
+
.oidc-btn:hover { filter: brightness(1.05); }
|
|
893
1062
|
</style>
|
|
894
1063
|
</head>
|
|
895
1064
|
<body>
|
|
896
|
-
<div class="login-card"
|
|
1065
|
+
<div class="login-card"__PATREON_ROLES_ATTR____OIDC_ATTR__>
|
|
897
1066
|
<h1>Sign in</h1>
|
|
898
1067
|
<p id="err" class="login-error" hidden></p>
|
|
899
1068
|
<form method="POST" action="/login">
|
|
@@ -918,6 +1087,10 @@ export const LOGIN_HTML = `<!doctype html>
|
|
|
918
1087
|
selector above only matters for password sign-in.
|
|
919
1088
|
</p>
|
|
920
1089
|
</div>
|
|
1090
|
+
<div id="oidc-section" hidden>
|
|
1091
|
+
<div class="login-divider">or</div>
|
|
1092
|
+
<a id="oidc-btn" class="oidc-btn" href="#">Sign in</a>
|
|
1093
|
+
</div>
|
|
921
1094
|
</div>
|
|
922
1095
|
<script>
|
|
923
1096
|
const params = new URLSearchParams(location.search);
|
|
@@ -936,6 +1109,12 @@ export const LOGIN_HTML = `<!doctype html>
|
|
|
936
1109
|
patreon_no_tier: "Signed in to Patreon, but your current pledge doesn't grant any of the configured tiers.",
|
|
937
1110
|
patreon_misconfigured: "Patreon login is misconfigured for this deploy.",
|
|
938
1111
|
patreon_failed: "Patreon sign-in failed.",
|
|
1112
|
+
oidc_state_mismatch: "Sign-in expired or was tampered with. Please try again.",
|
|
1113
|
+
oidc_token_exchange: "Couldn't complete sign-in with the identity provider. Try again.",
|
|
1114
|
+
oidc_identity: "Signed in, but the identity provider didn't return an email address.",
|
|
1115
|
+
oidc_no_role: "Signed in, but your account isn't authorized for any role here.",
|
|
1116
|
+
oidc_misconfigured: "Single sign-on is misconfigured for this deploy.",
|
|
1117
|
+
oidc_failed: "Sign-in failed.",
|
|
939
1118
|
};
|
|
940
1119
|
el.textContent = messages[err] || "Sign-in failed.";
|
|
941
1120
|
el.hidden = false;
|
|
@@ -948,6 +1127,14 @@ export const LOGIN_HTML = `<!doctype html>
|
|
|
948
1127
|
section.hidden = false;
|
|
949
1128
|
document.getElementById("patreon-btn").href = "/auth/patreon/start?next=" + encodeURIComponent(next);
|
|
950
1129
|
}
|
|
1130
|
+
// Same pattern for OIDC: the build stamps data-oidc with the provider's
|
|
1131
|
+
// display name when configured with at least one role rule.
|
|
1132
|
+
if (card.dataset.oidc) {
|
|
1133
|
+
document.getElementById("oidc-section").hidden = false;
|
|
1134
|
+
const oidcBtn = document.getElementById("oidc-btn");
|
|
1135
|
+
oidcBtn.textContent = "Sign in with " + card.dataset.oidc;
|
|
1136
|
+
oidcBtn.href = "/auth/oidc/start?next=" + encodeURIComponent(next);
|
|
1137
|
+
}
|
|
951
1138
|
// Autofocus moved here so it picks the right field whether the password
|
|
952
1139
|
// form is visible or the user is going for the Patreon button.
|
|
953
1140
|
const pwd = document.getElementById("password");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-template.js","sourceRoot":"","sources":["../../src/render/auth-template.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,+EAA+E;AAC/E,2EAA2E;AAC3E,mEAAmE;
|
|
1
|
+
{"version":3,"file":"auth-template.js","sourceRoot":"","sources":["../../src/render/auth-template.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,+EAA+E;AAC/E,2EAA2E;AAC3E,mEAAmE;AAuCnE,MAAM,UAAU,oBAAoB,CAAC,GAAuB;IAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC3D,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;IAC3D,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;IAErD,OAAO;;;;;;;gBAOO,YAAY;oBACR,gBAAgB;kBAClB,cAAc;eACjB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+9BzB,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAmIlB,CAAC"}
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
// The browser runtime (BATTLEMAP_RUNTIME below) ships as a built-in asset
|
|
23
23
|
// concatenated into _handlers.js; styles go into _handlers.css.
|
|
24
24
|
//
|
|
25
|
-
// Layer paths are vault-relative and resolve to the absolute served URL.
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// Layer paths are vault-relative and resolve to the absolute served URL. The
|
|
26
|
+
// build's per-variant asset scanner stages them via battlemapLayerPaths below,
|
|
27
|
+
// so a layer nothing else references (e.g. a web-only composited overlay)
|
|
28
|
+
// still ships with the deploy.
|
|
28
29
|
import yaml from "js-yaml";
|
|
29
30
|
import { htmlEscape } from "../../../escape.js";
|
|
30
31
|
import { registerBuiltinAssets } from "../assets.js";
|
|
@@ -35,6 +36,32 @@ function servedSrc(path) {
|
|
|
35
36
|
function errorBox(message) {
|
|
36
37
|
return { html: `<div class="vaults-bm-error">${htmlEscape(message)}</div>` };
|
|
37
38
|
}
|
|
39
|
+
// ```battlemap fenced blocks in raw markdown source.
|
|
40
|
+
const FENCE_RE = /^```battlemap[ \t]*\r?\n([\s\S]*?)^```/gm;
|
|
41
|
+
/** All layer image paths (vault-relative) named by ```battlemap blocks in a
|
|
42
|
+
* markdown source. Used by the build's asset scanner to stage layers into
|
|
43
|
+
* each variant; unparseable blocks contribute nothing. */
|
|
44
|
+
export function battlemapLayerPaths(source) {
|
|
45
|
+
const paths = [];
|
|
46
|
+
for (const match of source.matchAll(FENCE_RE)) {
|
|
47
|
+
let spec;
|
|
48
|
+
try {
|
|
49
|
+
spec = (yaml.load(match[1]) ?? {});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const lv of Array.isArray(spec.levels) ? spec.levels : []) {
|
|
55
|
+
if (!Array.isArray(lv?.layers))
|
|
56
|
+
continue;
|
|
57
|
+
for (const p of lv.layers) {
|
|
58
|
+
if (typeof p === "string" && p.length > 0)
|
|
59
|
+
paths.push(p);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return paths;
|
|
64
|
+
}
|
|
38
65
|
export const battlemapHandler = {
|
|
39
66
|
codeBlock: "battlemap",
|
|
40
67
|
render(content) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"battlemap.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/battlemap.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,EAAE;AACF,iBAAiB;AACjB,uFAAuF;AACvF,2FAA2F;AAC3F,0FAA0F;AAC1F,kFAAkF;AAClF,oEAAoE;AACpE,YAAY;AACZ,mBAAmB;AACnB,yDAAyD;AACzD,iEAAiE;AACjE,0BAA0B;AAC1B,gBAAgB;AAChB,iEAAiE;AACjE,wEAAwE;AACxE,QAAQ;AACR,EAAE;AACF,0EAA0E;AAC1E,gEAAgE;AAChE,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"battlemap.js","sourceRoot":"","sources":["../../../../src/render/handlers/builtin/battlemap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Pure email-to-role matching logic for the OIDC login flow, extracted so
|
|
2
|
+
// tests can exercise it without spinning up a Pages Function. The shipped
|
|
3
|
+
// middleware in auth-template.ts duplicates this logic verbatim — it has to
|
|
4
|
+
// live there as plain JS since the worker can't import TS modules at
|
|
5
|
+
// runtime. Keep the two copies in sync (small enough that drift is easy to
|
|
6
|
+
// spot in review).
|
|
7
|
+
/**
|
|
8
|
+
* Return the highest-ranked role whose rule matches the email, or null.
|
|
9
|
+
* Roles are ordered low → high; iterate from highest so a visitor matching
|
|
10
|
+
* several rules gets the strongest role, mirroring the Patreon tier matcher.
|
|
11
|
+
*/
|
|
12
|
+
export function matchOidcRole(email, roleRules, roles) {
|
|
13
|
+
const at = email.lastIndexOf("@");
|
|
14
|
+
if (at < 0)
|
|
15
|
+
return null;
|
|
16
|
+
const lower = email.toLowerCase();
|
|
17
|
+
const domain = lower.slice(at + 1);
|
|
18
|
+
for (let i = roles.length - 1; i >= 0; i--) {
|
|
19
|
+
const role = roles[i];
|
|
20
|
+
const rule = roleRules[role];
|
|
21
|
+
if (!rule)
|
|
22
|
+
continue;
|
|
23
|
+
if ((rule.emails ?? []).some((e) => e.toLowerCase() === lower))
|
|
24
|
+
return role;
|
|
25
|
+
if (domain && (rule.domains ?? []).some((d) => d.toLowerCase() === domain))
|
|
26
|
+
return role;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=oidc-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-match.js","sourceRoot":"","sources":["../../src/render/oidc-match.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,0EAA0E;AAC1E,4EAA4E;AAC5E,qEAAqE;AACrE,2EAA2E;AAC3E,mBAAmB;AAgBnB;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAa,EACb,SAAuC,EACvC,KAAe;IAEf,MAAM,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,EAAE,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;QACvB,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC5E,IAAI,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;IAC1F,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED