@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.
@@ -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) => ({ "&": "&amp;"
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
- const PATREON_STATE_COOKIE = "vault_patreon_state";
504
- const PATREON_STATE_TTL = 600; // 10 minutes long enough for the user to authorise
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(PATREON_STATE_COOKIE);
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
- // State cookie: stores { state, next } signed with SESSION_SECRET, used to
652
- // verify the OAuth callback came from our /start handler and not a forgery.
653
- // Lifetime is short (PATREON_STATE_TTL); kept distinct from the long-lived
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) + PATREON_STATE_TTL;
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 PATREON_STATE_COOKIE + "=" + value
662
- + "; Path=/; Secure; SameSite=Lax; HttpOnly; Max-Age=" + PATREON_STATE_TTL;
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[PATREON_STATE_COOKIE];
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"__PATREON_ROLES_ATTR__>
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;AAsBnE,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;IAE3D,OAAO;;;;;;gBAMO,YAAY;oBACR,gBAAgB;kBAClB,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAg0B/B,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0GlB,CAAC"}
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. They
26
- // must also be staged into the deploy true when the page already references
27
- // them (e.g. a Scene's foundry.data_json), which is the common case.
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wizzlethorpe/vaults",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Sync an Obsidian vault to a Cloudflare-hosted wiki with role-based access. Renders locally, deploys to Cloudflare Pages.",
5
5
  "keywords": [
6
6
  "obsidian",