cauthy-auth 0.5.2 → 0.8.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 +68 -3
- package/dist/{cauthy-BCTCqubt.d.ts → cauthy-Bn3iv50y.d.ts} +1 -1
- package/dist/{cauthy-hswMoS7B.d.mts → cauthy-C29HCvag.d.mts} +1 -1
- package/dist/cli.js +490 -25
- package/dist/client.d.mts +236 -0
- package/dist/client.d.ts +236 -0
- package/dist/client.js +331 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +297 -0
- package/dist/client.mjs.map +1 -0
- package/dist/express.d.mts +24 -0
- package/dist/express.d.ts +24 -0
- package/dist/express.js +102 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +76 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/fastify.js +3 -1
- package/dist/fastify.js.map +1 -1
- package/dist/fastify.mjs +3 -1
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +23 -0
- package/dist/hono.d.ts +23 -0
- package/dist/hono.js +50 -0
- package/dist/hono.js.map +1 -0
- package/dist/hono.mjs +24 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +44 -6
- package/dist/index.d.ts +44 -6
- package/dist/index.js +668 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +656 -37
- package/dist/index.mjs.map +1 -1
- package/dist/next.d.mts +24 -0
- package/dist/next.d.ts +24 -0
- package/dist/next.js +77 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +52 -0
- package/dist/next.mjs.map +1 -0
- package/dist/react.d.mts +546 -7
- package/dist/react.d.ts +546 -7
- package/dist/react.js +925 -14
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +913 -10
- package/dist/react.mjs.map +1 -1
- package/dist/styles.css +271 -0
- package/dist/svelte.d.mts +26 -0
- package/dist/svelte.d.ts +26 -0
- package/dist/svelte.js +402 -0
- package/dist/svelte.js.map +1 -0
- package/dist/svelte.mjs +373 -0
- package/dist/svelte.mjs.map +1 -0
- package/dist/{system-DvcQjORr.d.ts → system-DhRr_WxR.d.ts} +139 -1
- package/dist/{system-CR8p4BjB.d.mts → system-ZD_Wa8Bm.d.mts} +139 -1
- package/dist/theme.d.mts +56 -1
- package/dist/theme.d.ts +56 -1
- package/dist/theme.js +181 -5
- package/dist/theme.js.map +1 -1
- package/dist/theme.mjs +176 -4
- package/dist/theme.mjs.map +1 -1
- package/dist/validate-B_iC-4y8.d.mts +19 -0
- package/dist/validate-B_iC-4y8.d.ts +19 -0
- package/dist/vue.d.mts +28 -0
- package/dist/vue.d.ts +28 -0
- package/dist/vue.js +420 -0
- package/dist/vue.js.map +1 -0
- package/dist/vue.mjs +391 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +68 -7
- package/dist/permissions-B-a3eN0h.d.mts +0 -52
- package/dist/permissions-B-a3eN0h.d.ts +0 -52
package/dist/index.mjs
CHANGED
|
@@ -109,10 +109,92 @@ function applies(d, o) {
|
|
|
109
109
|
if (o.mode === "dark" && LIGHT.test(scope)) return false;
|
|
110
110
|
const named = NAMED.exec(scope);
|
|
111
111
|
if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
|
|
112
|
-
if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
|
|
112
|
+
if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host|^\.(dark|light)\b/.test(scope)) return false;
|
|
113
113
|
}
|
|
114
114
|
return true;
|
|
115
115
|
}
|
|
116
|
+
var TOKEN_CHARS = /^[A-Za-z0-9 #%.,()/'"_+*-]+$/;
|
|
117
|
+
var TOKEN_FUNCS = /* @__PURE__ */ new Set([
|
|
118
|
+
"var",
|
|
119
|
+
"calc",
|
|
120
|
+
"min",
|
|
121
|
+
"max",
|
|
122
|
+
"clamp",
|
|
123
|
+
"rgb",
|
|
124
|
+
"rgba",
|
|
125
|
+
"hsl",
|
|
126
|
+
"hsla",
|
|
127
|
+
"hwb",
|
|
128
|
+
"lab",
|
|
129
|
+
"lch",
|
|
130
|
+
"oklab",
|
|
131
|
+
"oklch",
|
|
132
|
+
"color",
|
|
133
|
+
"color-mix",
|
|
134
|
+
"light-dark",
|
|
135
|
+
"cubic-bezier",
|
|
136
|
+
"steps",
|
|
137
|
+
"linear",
|
|
138
|
+
"linear-gradient",
|
|
139
|
+
"radial-gradient",
|
|
140
|
+
"conic-gradient",
|
|
141
|
+
"repeating-linear-gradient",
|
|
142
|
+
"repeating-radial-gradient",
|
|
143
|
+
"drop-shadow",
|
|
144
|
+
"blur",
|
|
145
|
+
"translate",
|
|
146
|
+
"translatex",
|
|
147
|
+
"translatey",
|
|
148
|
+
"scale",
|
|
149
|
+
"rotate"
|
|
150
|
+
]);
|
|
151
|
+
function tokenValue(value) {
|
|
152
|
+
const v = value.replace(/\s+/g, " ").trim();
|
|
153
|
+
if (v === "" || v.length > 400 || !TOKEN_CHARS.test(v)) return null;
|
|
154
|
+
for (const m of v.matchAll(/([A-Za-z-]+)\(/g)) if (!TOKEN_FUNCS.has(m[1].toLowerCase())) return null;
|
|
155
|
+
if (/^\(|[ ,(]\(/.test(v)) return null;
|
|
156
|
+
return v;
|
|
157
|
+
}
|
|
158
|
+
var THEME_SCOPE = /^(?:\s*(?::root|html|body|\[data-(?:theme|mode|color-scheme)(?:=["']?[\w-]+["']?)?\])\s*,?)+$/i;
|
|
159
|
+
function tokensFromStackmyth(css, options = {}) {
|
|
160
|
+
const mode = options.mode ?? "light";
|
|
161
|
+
const all = (Array.isArray(css) ? css : [css]).flatMap(declarations);
|
|
162
|
+
const named = options.theme ?? all.map((d) => d.within.map((w) => NAMED.exec(w)?.[1]).find((n) => n && !/^(dark|light)$/i.test(n))).find(Boolean);
|
|
163
|
+
const vars = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const d of all) {
|
|
165
|
+
if (!applies(d, { mode, theme: named })) continue;
|
|
166
|
+
if (!d.within.every((w) => w.startsWith("@") || THEME_SCOPE.test(w))) continue;
|
|
167
|
+
vars.set(d.name, d.value);
|
|
168
|
+
}
|
|
169
|
+
const inline = (value, depth = 0) => {
|
|
170
|
+
if (depth > 16) return null;
|
|
171
|
+
let failed = false;
|
|
172
|
+
let out2 = value;
|
|
173
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
174
|
+
const next = out2.replace(/var\(\s*(--(?!sm-)[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
|
|
175
|
+
const known = vars.get(name);
|
|
176
|
+
const got = known !== void 0 ? inline(known, depth + 1) : fallback !== void 0 ? inline(fallback.trim(), depth + 1) : null;
|
|
177
|
+
if (got === null) failed = true;
|
|
178
|
+
return got ?? "";
|
|
179
|
+
});
|
|
180
|
+
if (failed) return null;
|
|
181
|
+
if (next === out2) break;
|
|
182
|
+
out2 = next;
|
|
183
|
+
}
|
|
184
|
+
return out2;
|
|
185
|
+
};
|
|
186
|
+
const tokens = {};
|
|
187
|
+
const warnings = [];
|
|
188
|
+
for (const [name, raw] of vars) {
|
|
189
|
+
if (!/^--sm-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) continue;
|
|
190
|
+
const written = inline(raw);
|
|
191
|
+
const value = written === null ? null : tokenValue(written);
|
|
192
|
+
if (value === null) warnings.push(`${name}: "${raw.length > 60 ? raw.slice(0, 57) + "\u2026" : raw}" is not a value Cauthy takes`);
|
|
193
|
+
else tokens[name] = value;
|
|
194
|
+
}
|
|
195
|
+
if (Object.keys(tokens).length === 0) warnings.push("no --sm-* token was found in these stylesheets");
|
|
196
|
+
return { tokens, warnings };
|
|
197
|
+
}
|
|
116
198
|
function resolve(value, vars, seen = 0) {
|
|
117
199
|
if (seen > 16) return null;
|
|
118
200
|
let unresolved = false;
|
|
@@ -259,7 +341,8 @@ function toWire(theme) {
|
|
|
259
341
|
panelIconUrl: "panel_icon_url",
|
|
260
342
|
texts: "texts",
|
|
261
343
|
inputBackground: "input_background",
|
|
262
|
-
inputBorder: "input_border"
|
|
344
|
+
inputBorder: "input_border",
|
|
345
|
+
tokens: "tokens"
|
|
263
346
|
};
|
|
264
347
|
const wire = {};
|
|
265
348
|
for (const [k, v] of Object.entries(theme)) {
|
|
@@ -293,18 +376,19 @@ function fromWire(wire) {
|
|
|
293
376
|
panel_icon_url: "panelIconUrl",
|
|
294
377
|
texts: "texts",
|
|
295
378
|
input_background: "inputBackground",
|
|
296
|
-
input_border: "inputBorder"
|
|
379
|
+
input_border: "inputBorder",
|
|
380
|
+
tokens: "tokens"
|
|
297
381
|
};
|
|
298
382
|
const theme = {};
|
|
299
383
|
for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
|
|
300
384
|
return theme;
|
|
301
385
|
}
|
|
302
386
|
async function pushTheme(o) {
|
|
303
|
-
const
|
|
304
|
-
const issuer = (o.issuer ??
|
|
305
|
-
const clientId = o.clientId ??
|
|
306
|
-
const tenant = o.tenant ??
|
|
307
|
-
const token = o.token ??
|
|
387
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
388
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
389
|
+
const clientId = o.clientId ?? env4("CAUTHY_CLIENT_ID") ?? "";
|
|
390
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
391
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
308
392
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
309
393
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
|
|
310
394
|
}
|
|
@@ -484,6 +568,11 @@ function createSystem(given = {}) {
|
|
|
484
568
|
return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
|
|
485
569
|
}
|
|
486
570
|
},
|
|
571
|
+
policy: {
|
|
572
|
+
async get(system) {
|
|
573
|
+
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
574
|
+
}
|
|
575
|
+
},
|
|
487
576
|
async publicJwks() {
|
|
488
577
|
if (!key) return { keys: [] };
|
|
489
578
|
const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
|
|
@@ -492,6 +581,45 @@ function createSystem(given = {}) {
|
|
|
492
581
|
};
|
|
493
582
|
}
|
|
494
583
|
|
|
584
|
+
// src/validate.ts
|
|
585
|
+
var MIN_PASSWORD = 8;
|
|
586
|
+
var MAX_PASSWORD = 256;
|
|
587
|
+
var MAX_NAME = 120;
|
|
588
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
589
|
+
function checkEmail(value) {
|
|
590
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
591
|
+
if (v === "") return { field: "email", code: "required", message: "Enter your email address." };
|
|
592
|
+
if (v.length > 254 || !EMAIL.test(v)) return { field: "email", code: "invalid_email", message: "That does not look like an email address." };
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function checkPassword(value) {
|
|
596
|
+
if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
|
|
597
|
+
if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
function checkNewPassword(value) {
|
|
601
|
+
const basic = checkPassword(value);
|
|
602
|
+
if (basic) return basic;
|
|
603
|
+
if (value.length < MIN_PASSWORD) {
|
|
604
|
+
return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
function checkName(value) {
|
|
609
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
610
|
+
if (typeof value !== "string" || value.trim().length > MAX_NAME) return { field: "name", code: "too_long", message: `Use at most ${MAX_NAME} characters.` };
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
function checkCode(value) {
|
|
614
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
615
|
+
if (v === "") return { field: "code", code: "required", message: "Enter the code." };
|
|
616
|
+
if (v.length > 40) return { field: "code", code: "too_long", message: "That is not a code." };
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
function firstProblem(...checks) {
|
|
620
|
+
return checks.find((c) => c !== null) ?? null;
|
|
621
|
+
}
|
|
622
|
+
|
|
495
623
|
// src/webhook.ts
|
|
496
624
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
497
625
|
var CauthyWebhookError = class extends Error {
|
|
@@ -513,8 +641,8 @@ function keysOf(issuer) {
|
|
|
513
641
|
}
|
|
514
642
|
var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
|
|
515
643
|
async function verifyWebhook(request, options = {}) {
|
|
516
|
-
const
|
|
517
|
-
const issuer = (options.issuer ??
|
|
644
|
+
const env4 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
|
|
645
|
+
const issuer = (options.issuer ?? env4 ?? "").replace(/\/+$/, "");
|
|
518
646
|
if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
|
|
519
647
|
const signature = request.headers.get("x-cauthy-signature-jwt");
|
|
520
648
|
if (!signature) throw new CauthyWebhookError("no signature");
|
|
@@ -565,9 +693,19 @@ async function verifyWebhook(request, options = {}) {
|
|
|
565
693
|
|
|
566
694
|
// src/cauthy.ts
|
|
567
695
|
var SESSION_DAYS = 30;
|
|
568
|
-
var FLOW_SECONDS =
|
|
696
|
+
var FLOW_SECONDS = 3600;
|
|
697
|
+
var AGAIN_SECONDS = 120;
|
|
569
698
|
var SKEW_SECONDS = 30;
|
|
570
699
|
var ORGANIZATIONS_TTL_MS = 6e4;
|
|
700
|
+
var LOOK_TTL_MS = 6e4;
|
|
701
|
+
var PROVIDER_ID = /^(custom:)?[a-z0-9_-]{1,40}$/;
|
|
702
|
+
function providerIn(segment) {
|
|
703
|
+
try {
|
|
704
|
+
return decodeURIComponent(segment);
|
|
705
|
+
} catch {
|
|
706
|
+
return "";
|
|
707
|
+
}
|
|
708
|
+
}
|
|
571
709
|
function env2(name) {
|
|
572
710
|
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
573
711
|
}
|
|
@@ -600,6 +738,7 @@ function createCauthy(given = {}) {
|
|
|
600
738
|
const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
|
|
601
739
|
const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
|
|
602
740
|
const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
|
|
741
|
+
const tenant = given.tenant ?? env2("CAUTHY_TENANT") ?? "";
|
|
603
742
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
|
|
604
743
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
605
744
|
if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
|
|
@@ -608,18 +747,26 @@ function createCauthy(given = {}) {
|
|
|
608
747
|
const afterSignOut = localPath(given.afterSignOut, "/");
|
|
609
748
|
const cookieName = given.cookieName ?? "cauthy_session";
|
|
610
749
|
const flowCookie = `${cookieName}_flow`;
|
|
750
|
+
const againCookie = `${cookieName}_again`;
|
|
751
|
+
const stepCookie = `${cookieName}_step`;
|
|
611
752
|
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
612
753
|
const key = keyOf(secret);
|
|
754
|
+
const pages = {
|
|
755
|
+
signIn: given.pages?.signIn ? localPath(given.pages.signIn, "/") : void 0,
|
|
756
|
+
resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
|
|
757
|
+
verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
|
|
758
|
+
};
|
|
759
|
+
const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
|
|
613
760
|
const originOf = (request) => (given.baseURL ?? new URL(request.url).origin).replace(/\/+$/, "");
|
|
614
761
|
const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
|
|
615
762
|
async function token(form) {
|
|
616
|
-
const
|
|
617
|
-
if (clientSecret)
|
|
763
|
+
const body2 = new URLSearchParams({ client_id: clientId, ...form });
|
|
764
|
+
if (clientSecret) body2.set("client_secret", clientSecret);
|
|
618
765
|
try {
|
|
619
766
|
const res = await doFetch(`${issuer}/v1/oauth/token`, {
|
|
620
767
|
method: "POST",
|
|
621
768
|
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
622
|
-
body
|
|
769
|
+
body: body2
|
|
623
770
|
});
|
|
624
771
|
if (res.ok) return { ok: true, body: await res.json() };
|
|
625
772
|
return { ok: false, ended: res.status >= 400 && res.status < 500 };
|
|
@@ -649,12 +796,12 @@ function createCauthy(given = {}) {
|
|
|
649
796
|
return null;
|
|
650
797
|
}
|
|
651
798
|
}
|
|
652
|
-
async function stored(
|
|
653
|
-
if (typeof
|
|
654
|
-
const who = await userinfo(
|
|
799
|
+
async function stored(body2) {
|
|
800
|
+
if (typeof body2?.access_token !== "string" || typeof body2?.refresh_token !== "string") return null;
|
|
801
|
+
const who = await userinfo(body2.access_token);
|
|
655
802
|
if (who === null || who.u.id === "") return null;
|
|
656
|
-
const lifetime = typeof
|
|
657
|
-
return { at:
|
|
803
|
+
const lifetime = typeof body2.expires_in === "number" && body2.expires_in > 0 ? body2.expires_in : 300;
|
|
804
|
+
return { at: body2.access_token, rt: body2.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who };
|
|
658
805
|
}
|
|
659
806
|
function toSession(s) {
|
|
660
807
|
return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1e3).toISOString() };
|
|
@@ -728,8 +875,8 @@ function createCauthy(given = {}) {
|
|
|
728
875
|
const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
|
|
729
876
|
const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
|
|
730
877
|
if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`);
|
|
731
|
-
const
|
|
732
|
-
const value = (
|
|
878
|
+
const body2 = await res.json();
|
|
879
|
+
const value = (body2.data ?? []).map(
|
|
733
880
|
(o) => ({
|
|
734
881
|
id: String(o.id),
|
|
735
882
|
system: String(o.system ?? ""),
|
|
@@ -774,20 +921,28 @@ function createCauthy(given = {}) {
|
|
|
774
921
|
if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
|
|
775
922
|
const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
|
|
776
923
|
if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
|
|
777
|
-
|
|
924
|
+
const passkey = url.searchParams.get("method") === "passkey" ? "#passkey" : "";
|
|
925
|
+
return redirect(`${issuer}/v1/oauth/authorize?${q}${passkey}`, [
|
|
778
926
|
writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS))
|
|
779
927
|
]);
|
|
780
928
|
}
|
|
781
929
|
function failed(request, reason) {
|
|
782
930
|
const h = new Headers({ "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
783
931
|
h.append("set-cookie", writeCookie(flowCookie, "", attrs(request, 0)));
|
|
932
|
+
h.append("set-cookie", writeCookie(againCookie, "", attrs(request, 0)));
|
|
784
933
|
return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h });
|
|
785
934
|
}
|
|
786
935
|
async function callback(request, url) {
|
|
787
936
|
const had = readCookies(request.headers.get("cookie"));
|
|
788
937
|
const sealed = had.get(flowCookie);
|
|
789
938
|
const flow = sealed ? await unseal(sealed, await key) : null;
|
|
790
|
-
if (flow === null || url.searchParams.get("state") !== flow.state)
|
|
939
|
+
if (flow === null || url.searchParams.get("state") !== flow.state) {
|
|
940
|
+
if (had.get(againCookie) === void 0) {
|
|
941
|
+
const back = flow?.returnTo ?? afterSignIn;
|
|
942
|
+
return redirect(`${basePath}/login?return_to=${encodeURIComponent(back)}`, [writeCookie(againCookie, "1", attrs(request, AGAIN_SECONDS))]);
|
|
943
|
+
}
|
|
944
|
+
return failed(request, "state_mismatch");
|
|
945
|
+
}
|
|
791
946
|
const error = url.searchParams.get("error");
|
|
792
947
|
if (error !== null) {
|
|
793
948
|
if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
@@ -803,28 +958,332 @@ function createCauthy(given = {}) {
|
|
|
803
958
|
});
|
|
804
959
|
const s = res.ok ? await stored(res.body) : null;
|
|
805
960
|
if (s === null) return failed(request, "exchange_failed");
|
|
806
|
-
return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
961
|
+
return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]);
|
|
807
962
|
}
|
|
808
963
|
async function logout(request) {
|
|
809
964
|
const { had, value } = await read(request);
|
|
810
965
|
if (value !== null) {
|
|
811
|
-
const
|
|
812
|
-
if (clientSecret)
|
|
966
|
+
const body2 = new URLSearchParams({ client_id: clientId, token: value.rt });
|
|
967
|
+
if (clientSecret) body2.set("client_secret", clientSecret);
|
|
813
968
|
await doFetch(`${issuer}/v1/oauth/revoke`, {
|
|
814
969
|
method: "POST",
|
|
815
970
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
816
|
-
body
|
|
971
|
+
body: body2
|
|
817
972
|
}).catch(() => void 0);
|
|
818
973
|
orgCache.delete(value.at);
|
|
819
974
|
}
|
|
820
975
|
const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
|
|
821
976
|
return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
|
|
822
977
|
}
|
|
978
|
+
const STEP_SECONDS = 600;
|
|
979
|
+
const problem = (status2, code, message, field, headers) => json({ error: { code, message, ...field ? { field } : {} } }, status2, headers);
|
|
980
|
+
const invalid = (p) => problem(400, p.code, p.message, p.field);
|
|
981
|
+
function ownRequest(request) {
|
|
982
|
+
const origin = request.headers.get("origin");
|
|
983
|
+
if (origin !== null) return origin === originOf(request) || origin === new URL(request.url).origin;
|
|
984
|
+
return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
|
|
985
|
+
}
|
|
986
|
+
async function body(request) {
|
|
987
|
+
if (!(request.headers.get("content-type") ?? "").includes("application/json")) return null;
|
|
988
|
+
try {
|
|
989
|
+
const b = await request.json();
|
|
990
|
+
return typeof b === "object" && b !== null && !Array.isArray(b) ? b : null;
|
|
991
|
+
} catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const clientIp = given.clientIp ?? ((request) => request.headers.get("x-forwarded-for")?.split(",").pop()?.trim() || void 0);
|
|
996
|
+
let noProofUntil = 0;
|
|
997
|
+
async function passedOn(request) {
|
|
998
|
+
const ip = clientIp(request);
|
|
999
|
+
if (!ip || Date.now() < noProofUntil) return {};
|
|
1000
|
+
try {
|
|
1001
|
+
return { "x-cauthy-client-ip": ip, "x-cauthy-system-token": await ownSystem.token() };
|
|
1002
|
+
} catch {
|
|
1003
|
+
noProofUntil = Date.now() + 5 * 6e4;
|
|
1004
|
+
return {};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function ask(request, method, path, payload, page, bearer2) {
|
|
1008
|
+
try {
|
|
1009
|
+
const res = await doFetch(`${issuer}${path}`, {
|
|
1010
|
+
method,
|
|
1011
|
+
headers: {
|
|
1012
|
+
"x-tenant-key": tenant,
|
|
1013
|
+
accept: "application/json",
|
|
1014
|
+
// A link Cauthy mails opens the application's page for it when it
|
|
1015
|
+
// has one; otherwise Cauthy's own. Either way in the reader's language.
|
|
1016
|
+
...page ? { "x-cauthy-link-to": `${originOf(request)}${page}`, "x-cauthy-client-id": clientId } : { "x-cauthy-links": "hosted" },
|
|
1017
|
+
...await passedOn(request),
|
|
1018
|
+
...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
|
|
1019
|
+
...request.headers.get("accept-language") ? { "accept-language": request.headers.get("accept-language") } : {},
|
|
1020
|
+
...request.headers.get("user-agent") ? { "user-agent": request.headers.get("user-agent") } : {},
|
|
1021
|
+
...payload === void 0 ? {} : { "content-type": "application/json" }
|
|
1022
|
+
},
|
|
1023
|
+
body: payload === void 0 ? void 0 : JSON.stringify(payload)
|
|
1024
|
+
});
|
|
1025
|
+
const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
|
|
1026
|
+
if (res.ok) return { status: res.status, data: b?.data ?? b };
|
|
1027
|
+
const first = b?.errors?.[0];
|
|
1028
|
+
return { status: res.status, data: null, error: { code: String(first?.code ?? "error"), message: String(first?.message ?? "The request could not be completed.") } };
|
|
1029
|
+
} catch {
|
|
1030
|
+
return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const accountOf = (d) => ({
|
|
1034
|
+
id: String(d?.id ?? ""),
|
|
1035
|
+
email: String(d?.email ?? ""),
|
|
1036
|
+
name: String(d?.name ?? ""),
|
|
1037
|
+
emailVerified: d?.email_verified === true,
|
|
1038
|
+
hasPassword: d?.has_password === true,
|
|
1039
|
+
mfaEnabled: d?.mfa_enabled === true
|
|
1040
|
+
});
|
|
1041
|
+
async function signedInAs(request) {
|
|
1042
|
+
const r = await resolve2(request);
|
|
1043
|
+
const headers = new Headers();
|
|
1044
|
+
for (const c of r.cookies) headers.append("set-cookie", c);
|
|
1045
|
+
if (r.session === null) return { response: problem(401, "unauthenticated", "Sign in first.", void 0, headers) };
|
|
1046
|
+
return { token: r.session.accessToken, kept: r.cookies.length === 0 ? (await read(request)).value : null, headers };
|
|
1047
|
+
}
|
|
1048
|
+
async function signedIn(request, tokens, extra = []) {
|
|
1049
|
+
const s = await stored(tokens);
|
|
1050
|
+
if (s === null) return problem(502, "unavailable", "Sign-in is unavailable right now. Try again in a moment.");
|
|
1051
|
+
const h = new Headers();
|
|
1052
|
+
for (const c of [...await write(request, s, readCookies(request.headers.get("cookie"))), ...extra]) h.append("set-cookie", c);
|
|
1053
|
+
return json({ status: "signed_in", session: await toPublic(toSession(s)) }, 200, h);
|
|
1054
|
+
}
|
|
1055
|
+
let lookCache = null;
|
|
1056
|
+
async function ownRoute(request, url, route) {
|
|
1057
|
+
const post = request.method === "POST";
|
|
1058
|
+
const own = ["/sign-in/email", "/sign-in/mfa", "/sign-up/email", "/forgot-password", "/reset-password", "/verify-email", "/sign-in/magic-link", "/sign-in/magic-link/verify", "/sign-in/email-code", "/me", "/me/password", "/me/mfa/setup", "/me/mfa/verify", "/me/mfa/disable", "/me/mfa/recovery-codes", "/me/sessions", "/me/sessions/end", "/sign-out", "/methods", "/look"].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/");
|
|
1059
|
+
if (!own) return null;
|
|
1060
|
+
if (tenant === "") {
|
|
1061
|
+
return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
|
|
1062
|
+
}
|
|
1063
|
+
if (route === "/look" && !post) {
|
|
1064
|
+
if (lookCache === null || lookCache.until < Date.now()) {
|
|
1065
|
+
const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(clientId)}`);
|
|
1066
|
+
lookCache = { until: Date.now() + LOOK_TTL_MS, look: res.error ? {} : res.data ?? {} };
|
|
1067
|
+
}
|
|
1068
|
+
return json(lookCache.look);
|
|
1069
|
+
}
|
|
1070
|
+
if (route === "/methods" && !post) {
|
|
1071
|
+
const [providers, settings] = await Promise.all([ask(request, "GET", "/v1/auth/social/providers"), ask(request, "GET", "/v1/auth/settings")]);
|
|
1072
|
+
const list = Array.isArray(providers.data) ? providers.data : [];
|
|
1073
|
+
return json({
|
|
1074
|
+
providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
|
|
1075
|
+
signUp: settings.data?.allow_signup !== false,
|
|
1076
|
+
// A sign-in link needs a page of the application to open.
|
|
1077
|
+
magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
|
|
1078
|
+
// Asked for on Cauthy's page: the person goes there for that step and comes back.
|
|
1079
|
+
passkey: true
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (route.startsWith("/sign-in/social/") && !post) {
|
|
1083
|
+
const provider = providerIn(route.slice("/sign-in/social/".length));
|
|
1084
|
+
if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
|
|
1085
|
+
const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
1086
|
+
const callback2 = `${basePath}/callback/${provider}`;
|
|
1087
|
+
const started = await ask(request, "GET", `/v1/auth/social/${provider}?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2);
|
|
1088
|
+
if (started.error || typeof started.data?.redirect_url !== "string") {
|
|
1089
|
+
return redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(started.error?.code ?? "provider_unavailable")}`);
|
|
1090
|
+
}
|
|
1091
|
+
const step = { kind: "social", token: String(started.data.state ?? ""), provider, returnTo };
|
|
1092
|
+
return redirect(started.data.redirect_url, [writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS))]);
|
|
1093
|
+
}
|
|
1094
|
+
if (route.startsWith("/callback/") && !post) {
|
|
1095
|
+
const provider = providerIn(route.slice("/callback/".length));
|
|
1096
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1097
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1098
|
+
const clear = writeCookie(stepCookie, "", attrs(request, 0));
|
|
1099
|
+
const back = step?.returnTo ?? afterSignIn;
|
|
1100
|
+
const failedTo = (code2) => redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code2)}`, [clear]);
|
|
1101
|
+
if (step === null || step.kind !== "social" || step.provider !== provider || url.searchParams.get("state") !== step.token) return failedTo("state_mismatch");
|
|
1102
|
+
const code = url.searchParams.get("code");
|
|
1103
|
+
if (!code) return failedTo(url.searchParams.get("error") === "access_denied" ? "cancelled" : "provider_refused");
|
|
1104
|
+
const q = new URLSearchParams({ code, state: step.token, redirect_uri: `${originOf(request)}${basePath}/callback/${provider}` });
|
|
1105
|
+
const done = await ask(request, "GET", `/v1/auth/social/${provider}/callback?${q}`);
|
|
1106
|
+
if (done.error) return failedTo(done.error.code);
|
|
1107
|
+
const s = await stored(done.data);
|
|
1108
|
+
if (s === null) return failedTo("unavailable");
|
|
1109
|
+
return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
|
|
1110
|
+
}
|
|
1111
|
+
if ((route === "/me" || route === "/me/sessions") && !post) {
|
|
1112
|
+
const mine = await signedInAs(request);
|
|
1113
|
+
if ("response" in mine) return mine.response;
|
|
1114
|
+
const res = await ask(request, "GET", route === "/me" ? "/v1/auth/me" : "/v1/auth/me/sessions", void 0, void 0, mine.token);
|
|
1115
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
1116
|
+
if (route === "/me/sessions") {
|
|
1117
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
1118
|
+
return json({ sessions: list.map((x) => ({ device: String(x.device ?? ""), ip: String(x.ip ?? ""), createdAt: String(x.created_at ?? ""), expiresAt: String(x.expires_at ?? "") })) }, 200, mine.headers);
|
|
1119
|
+
}
|
|
1120
|
+
return json({ account: accountOf(res.data) }, 200, mine.headers);
|
|
1121
|
+
}
|
|
1122
|
+
if (!post) return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
1123
|
+
if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
|
|
1124
|
+
if (route === "/sign-out") {
|
|
1125
|
+
const { had, value } = await read(request);
|
|
1126
|
+
if (value !== null) {
|
|
1127
|
+
const form = new URLSearchParams({ client_id: clientId, token: value.rt });
|
|
1128
|
+
if (clientSecret) form.set("client_secret", clientSecret);
|
|
1129
|
+
await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
|
|
1130
|
+
orgCache.delete(value.at);
|
|
1131
|
+
}
|
|
1132
|
+
const h = new Headers();
|
|
1133
|
+
for (const c of clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
|
|
1134
|
+
return json({ status: "signed_out" }, 200, h);
|
|
1135
|
+
}
|
|
1136
|
+
const b = await body(request);
|
|
1137
|
+
if (b === null) return problem(400, "bad_request", "Send the form as JSON.");
|
|
1138
|
+
if (route === "/sign-in/email") {
|
|
1139
|
+
const bad = firstProblem(checkEmail(b.email), checkPassword(b.password));
|
|
1140
|
+
if (bad) return invalid(bad);
|
|
1141
|
+
const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password });
|
|
1142
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1143
|
+
if (res.data?.mfa_required === true) {
|
|
1144
|
+
const step = { kind: "mfa", token: String(res.data.mfa_token ?? ""), returnTo: afterSignIn };
|
|
1145
|
+
return json({ status: "mfa_required" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
|
|
1146
|
+
}
|
|
1147
|
+
return signedIn(request, res.data);
|
|
1148
|
+
}
|
|
1149
|
+
if (route === "/sign-in/mfa") {
|
|
1150
|
+
const bad = checkCode(b.code);
|
|
1151
|
+
if (bad) return invalid(bad);
|
|
1152
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1153
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1154
|
+
if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
|
|
1155
|
+
const res = await ask(request, "POST", "/v1/auth/mfa/challenge", { mfa_token: step.token, code: String(b.code).trim() });
|
|
1156
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
|
|
1157
|
+
return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
|
|
1158
|
+
}
|
|
1159
|
+
if (route === "/sign-up/email") {
|
|
1160
|
+
const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password));
|
|
1161
|
+
if (bad) return invalid(bad);
|
|
1162
|
+
const email = String(b.email).trim();
|
|
1163
|
+
const made = await ask(request, "POST", "/v1/auth/register", { email, password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "" }, pages.verifyEmail);
|
|
1164
|
+
if (made.error) return problem(made.status, made.error.code, made.error.message);
|
|
1165
|
+
if (made.data?.verification_required === true) return json({ status: "verification_required", email });
|
|
1166
|
+
const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
|
|
1167
|
+
if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
|
|
1168
|
+
return signedIn(request, res.data);
|
|
1169
|
+
}
|
|
1170
|
+
if (route === "/forgot-password") {
|
|
1171
|
+
const bad = checkEmail(b.email);
|
|
1172
|
+
if (bad) return invalid(bad);
|
|
1173
|
+
await ask(request, "POST", "/v1/auth/password/reset", { email: String(b.email).trim() }, pages.resetPassword);
|
|
1174
|
+
return json({ status: "sent" });
|
|
1175
|
+
}
|
|
1176
|
+
if (route.startsWith("/me")) {
|
|
1177
|
+
const mine = await signedInAs(request);
|
|
1178
|
+
if ("response" in mine) return mine.response;
|
|
1179
|
+
const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
|
|
1180
|
+
const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers);
|
|
1181
|
+
const password = typeof b.password === "string" ? b.password : "";
|
|
1182
|
+
if (route === "/me") {
|
|
1183
|
+
const bad = checkName(b.name);
|
|
1184
|
+
if (bad) return invalid(bad);
|
|
1185
|
+
const res = await as("/v1/auth/me", { name: typeof b.name === "string" ? b.name.trim() : "" }, "PATCH");
|
|
1186
|
+
if (res.error) return refused(res, "name");
|
|
1187
|
+
const h = mine.headers;
|
|
1188
|
+
if (mine.kept !== null) {
|
|
1189
|
+
const again = await stored({ access_token: mine.kept.at, refresh_token: mine.kept.rt, expires_in: Math.max(1, mine.kept.exp - Math.floor(Date.now() / 1e3)) });
|
|
1190
|
+
if (again !== null) for (const c of await write(request, again, readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
|
|
1191
|
+
}
|
|
1192
|
+
return json({ status: "saved", account: accountOf(res.data) }, 200, h);
|
|
1193
|
+
}
|
|
1194
|
+
if (route === "/me/password") {
|
|
1195
|
+
const bad = checkNewPassword(b.newPassword);
|
|
1196
|
+
if (bad) return problem(400, bad.code, bad.message, "newPassword");
|
|
1197
|
+
const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
|
|
1198
|
+
if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
|
|
1199
|
+
const next = await stored(res.data);
|
|
1200
|
+
const h = new Headers();
|
|
1201
|
+
const had = readCookies(request.headers.get("cookie"));
|
|
1202
|
+
for (const c of next !== null ? await write(request, next, had) : clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
|
|
1203
|
+
return json({ status: "changed", signedIn: next !== null }, 200, h);
|
|
1204
|
+
}
|
|
1205
|
+
if (route === "/me/mfa/setup") {
|
|
1206
|
+
const res = await as("/v1/auth/mfa/setup", { password });
|
|
1207
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
1208
|
+
return json({ status: "started", secret: String(res.data?.secret ?? ""), otpauthUri: String(res.data?.otpauth_uri ?? "") }, 200, mine.headers);
|
|
1209
|
+
}
|
|
1210
|
+
if (route === "/me/mfa/verify") {
|
|
1211
|
+
const bad = checkCode(b.code);
|
|
1212
|
+
if (bad) return invalid(bad);
|
|
1213
|
+
const res = await as("/v1/auth/mfa/verify", { code: String(b.code).trim() });
|
|
1214
|
+
if (res.error) return refused(res, "code");
|
|
1215
|
+
return json({ status: "enabled", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
|
|
1216
|
+
}
|
|
1217
|
+
if (route === "/me/mfa/disable") {
|
|
1218
|
+
const res = await as("/v1/auth/me/mfa/disable", { password });
|
|
1219
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
1220
|
+
return json({ status: "disabled" }, 200, mine.headers);
|
|
1221
|
+
}
|
|
1222
|
+
if (route === "/me/mfa/recovery-codes") {
|
|
1223
|
+
const res = await as("/v1/auth/me/mfa/recovery-codes", { password });
|
|
1224
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
1225
|
+
return json({ status: "renewed", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
|
|
1226
|
+
}
|
|
1227
|
+
if (route === "/me/sessions/end") {
|
|
1228
|
+
const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
|
|
1229
|
+
if (res.error) return refused(res);
|
|
1230
|
+
const h = new Headers();
|
|
1231
|
+
for (const c of clearParts(cookieName, attrs(request, 0), readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
|
|
1232
|
+
orgCache.delete(mine.token);
|
|
1233
|
+
return json({ status: "signed_out" }, 200, h);
|
|
1234
|
+
}
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
const isToken = (v) => typeof v === "string" && v !== "" && v.length <= 512;
|
|
1238
|
+
const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
|
|
1239
|
+
if (route === "/reset-password") {
|
|
1240
|
+
if (!isToken(b.token)) return badLink();
|
|
1241
|
+
const bad = checkNewPassword(b.password);
|
|
1242
|
+
if (bad) return invalid(bad);
|
|
1243
|
+
const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
|
|
1244
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0);
|
|
1245
|
+
return json({ status: "changed" });
|
|
1246
|
+
}
|
|
1247
|
+
if (route === "/verify-email") {
|
|
1248
|
+
if (!isToken(b.token)) return badLink();
|
|
1249
|
+
const res = await ask(request, "POST", "/v1/auth/email/verify", { token: String(b.token) });
|
|
1250
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1251
|
+
return json({ status: "verified" });
|
|
1252
|
+
}
|
|
1253
|
+
if (route === "/sign-in/magic-link") {
|
|
1254
|
+
const bad = checkEmail(b.email);
|
|
1255
|
+
if (bad) return invalid(bad);
|
|
1256
|
+
if (pages.signIn === void 0) return problem(500, "not_configured", "cauthy-auth: pages.signIn is required for sign-in links: it is the page the link opens");
|
|
1257
|
+
const email = String(b.email).trim();
|
|
1258
|
+
await ask(request, "POST", "/v1/auth/magic-link/request", { email }, pages.signIn);
|
|
1259
|
+
const step = { kind: "code", token: email, returnTo: afterSignIn };
|
|
1260
|
+
return json({ status: "sent" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
|
|
1261
|
+
}
|
|
1262
|
+
if (route === "/sign-in/email-code") {
|
|
1263
|
+
const bad = checkCode(b.code);
|
|
1264
|
+
if (bad) return invalid(bad);
|
|
1265
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1266
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1267
|
+
if (step === null || step.kind !== "code") return problem(400, "step_expired", "Ask for a new link: this one was asked for too long ago.");
|
|
1268
|
+
const res = await ask(request, "POST", "/v1/auth/magic-link/verify-code", { email: step.token, code: String(b.code).trim() });
|
|
1269
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
|
|
1270
|
+
return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
|
|
1271
|
+
}
|
|
1272
|
+
if (route === "/sign-in/magic-link/verify") {
|
|
1273
|
+
if (!isToken(b.token)) return badLink();
|
|
1274
|
+
const res = await ask(request, "POST", "/v1/auth/magic-link/verify", { token: String(b.token) });
|
|
1275
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1276
|
+
return signedIn(request, res.data);
|
|
1277
|
+
}
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
823
1280
|
async function handler(request) {
|
|
824
1281
|
const url = new URL(request.url);
|
|
825
1282
|
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
|
|
826
1283
|
const route = url.pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
|
|
827
1284
|
const get = request.method === "GET" || request.method === "HEAD";
|
|
1285
|
+
const mine = await ownRoute(request, url, route);
|
|
1286
|
+
if (mine !== null) return mine;
|
|
828
1287
|
if (route === "/login" && get) return login(request, url);
|
|
829
1288
|
if (route === "/callback" && get) return callback(request, url);
|
|
830
1289
|
if (route === "/logout" && (get || request.method === "POST")) return logout(request);
|
|
@@ -868,7 +1327,7 @@ function createCauthy(given = {}) {
|
|
|
868
1327
|
return {
|
|
869
1328
|
handler,
|
|
870
1329
|
webhook,
|
|
871
|
-
system:
|
|
1330
|
+
system: ownSystem,
|
|
872
1331
|
getSession,
|
|
873
1332
|
status,
|
|
874
1333
|
resolve: resolve2,
|
|
@@ -893,12 +1352,101 @@ function can(organization, resource, action) {
|
|
|
893
1352
|
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
894
1353
|
return organization.permissions.some((p) => matches(p, resource, action));
|
|
895
1354
|
}
|
|
1355
|
+
function definePolicy(roles) {
|
|
1356
|
+
const entry = (p) => `${p.resource}:${p.action}`;
|
|
1357
|
+
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
1358
|
+
const split = (permission) => {
|
|
1359
|
+
const i = permission.lastIndexOf(":");
|
|
1360
|
+
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
1361
|
+
};
|
|
1362
|
+
return {
|
|
1363
|
+
roles: roles.map((r) => ({
|
|
1364
|
+
role: r.role,
|
|
1365
|
+
...r.name !== void 0 ? { name: r.name } : {},
|
|
1366
|
+
...r.description !== void 0 ? { description: r.description } : {},
|
|
1367
|
+
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
1368
|
+
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
1369
|
+
})),
|
|
1370
|
+
roleNames: roles.map((r) => r.role),
|
|
1371
|
+
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
1372
|
+
can(organization, permission) {
|
|
1373
|
+
const [resource, action] = split(permission);
|
|
1374
|
+
return can(organization, resource, action);
|
|
1375
|
+
},
|
|
1376
|
+
hasRole(organization, ...wanted) {
|
|
1377
|
+
return organization != null && wanted.includes(organization.role);
|
|
1378
|
+
},
|
|
1379
|
+
permissionsOf(role) {
|
|
1380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1381
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
1382
|
+
const walk = (name) => {
|
|
1383
|
+
if (seen.has(name)) return;
|
|
1384
|
+
seen.add(name);
|
|
1385
|
+
const r = byName.get(name);
|
|
1386
|
+
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
1387
|
+
for (const parent of r?.inherits ?? []) walk(parent);
|
|
1388
|
+
};
|
|
1389
|
+
walk(role);
|
|
1390
|
+
return [...out2];
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1395
|
+
function policySource(system, roles) {
|
|
1396
|
+
const text = (v) => JSON.stringify(v);
|
|
1397
|
+
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
1398
|
+
const one = (r) => {
|
|
1399
|
+
const parts = [`role: ${text(r.role)}`];
|
|
1400
|
+
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
1401
|
+
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
1402
|
+
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
1403
|
+
if (r.permissions?.length) {
|
|
1404
|
+
const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
|
|
1405
|
+
parts.push(`permissions: [
|
|
1406
|
+
${list.join("\n")}
|
|
1407
|
+
]`);
|
|
1408
|
+
}
|
|
1409
|
+
return ` {
|
|
1410
|
+
${parts.join(",\n ")},
|
|
1411
|
+
},`;
|
|
1412
|
+
};
|
|
1413
|
+
return `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
1414
|
+
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
1415
|
+
// changes, and do not edit this file by hand.
|
|
1416
|
+
|
|
1417
|
+
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
1418
|
+
|
|
1419
|
+
export const policy = definePolicy([
|
|
1420
|
+
${roles.map(one).join("\n")}
|
|
1421
|
+
])
|
|
1422
|
+
|
|
1423
|
+
export const { can, hasRole } = policy
|
|
1424
|
+
export type Role = RoleOf<typeof policy>
|
|
1425
|
+
export type Permission = PermissionOf<typeof policy>
|
|
1426
|
+
`;
|
|
1427
|
+
}
|
|
1428
|
+
async function pullPolicy(o = {}) {
|
|
1429
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
1430
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1431
|
+
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
1432
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
1433
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
1434
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
1435
|
+
if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
|
|
1436
|
+
}
|
|
1437
|
+
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
1438
|
+
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
|
|
1439
|
+
});
|
|
1440
|
+
const body = await res.json().catch(() => null);
|
|
1441
|
+
if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
1442
|
+
return body?.data?.roles ?? [];
|
|
1443
|
+
}
|
|
896
1444
|
async function pushPolicy(o) {
|
|
897
|
-
const
|
|
898
|
-
const issuer = (o.issuer ??
|
|
899
|
-
const system = o.system ??
|
|
900
|
-
const tenant = o.tenant ??
|
|
901
|
-
const token = o.token ??
|
|
1445
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
1446
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1447
|
+
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
1448
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
1449
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
902
1450
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
903
1451
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
|
|
904
1452
|
}
|
|
@@ -921,10 +1469,10 @@ function sessionSecret() {
|
|
|
921
1469
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
922
1470
|
}
|
|
923
1471
|
async function connectSystem(o) {
|
|
924
|
-
const
|
|
925
|
-
const issuer = (o.issuer ??
|
|
926
|
-
const tenant = o.tenant ??
|
|
927
|
-
const token = o.token ??
|
|
1472
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
1473
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1474
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
1475
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
928
1476
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
929
1477
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
|
|
930
1478
|
}
|
|
@@ -956,17 +1504,88 @@ async function connectSystem(o) {
|
|
|
956
1504
|
env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
|
|
957
1505
|
};
|
|
958
1506
|
}
|
|
1507
|
+
|
|
1508
|
+
// src/verify.ts
|
|
1509
|
+
import { createRemoteJWKSet as createRemoteJWKSet2, jwtVerify as jwtVerify2 } from "jose";
|
|
1510
|
+
var CauthyTokenError = class extends Error {
|
|
1511
|
+
constructor(reason) {
|
|
1512
|
+
super(`cauthy-auth: token refused: ${reason}`);
|
|
1513
|
+
this.reason = reason;
|
|
1514
|
+
this.name = "CauthyTokenError";
|
|
1515
|
+
}
|
|
1516
|
+
reason;
|
|
1517
|
+
};
|
|
1518
|
+
var keys2 = /* @__PURE__ */ new Map();
|
|
1519
|
+
function keysOf2(issuer) {
|
|
1520
|
+
let set = keys2.get(issuer);
|
|
1521
|
+
if (set === void 0) {
|
|
1522
|
+
set = createRemoteJWKSet2(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
|
|
1523
|
+
keys2.set(issuer, set);
|
|
1524
|
+
}
|
|
1525
|
+
return set;
|
|
1526
|
+
}
|
|
1527
|
+
function env3(name) {
|
|
1528
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
1529
|
+
}
|
|
1530
|
+
function bearer(from) {
|
|
1531
|
+
if (typeof from === "string") return from.replace(/^Bearer\s+/i, "").trim();
|
|
1532
|
+
const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
|
|
1533
|
+
return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
|
|
1534
|
+
}
|
|
1535
|
+
async function verifyToken(from, options = {}) {
|
|
1536
|
+
const issuer = (options.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1537
|
+
if (issuer === "") throw new CauthyTokenError("issuer (CAUTHY_ISSUER) is not set");
|
|
1538
|
+
const token = bearer(from);
|
|
1539
|
+
if (token === "") throw new CauthyTokenError("no token");
|
|
1540
|
+
let claims;
|
|
1541
|
+
try {
|
|
1542
|
+
const verified = await jwtVerify2(token, options.jwks ?? keysOf2(issuer), {
|
|
1543
|
+
algorithms: ["RS256"],
|
|
1544
|
+
issuer,
|
|
1545
|
+
audience: options.audience ?? env3("CAUTHY_AUDIENCE"),
|
|
1546
|
+
requiredClaims: ["exp", "sub"],
|
|
1547
|
+
currentDate: options.now
|
|
1548
|
+
});
|
|
1549
|
+
claims = verified.payload;
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
throw new CauthyTokenError(err instanceof Error ? err.message : "bad signature");
|
|
1552
|
+
}
|
|
1553
|
+
const tenantId = typeof claims.tid === "string" ? claims.tid : "";
|
|
1554
|
+
const wanted = options.tenantId ?? env3("CAUTHY_TENANT_ID");
|
|
1555
|
+
if (wanted && tenantId !== wanted) throw new CauthyTokenError("issued for another workspace");
|
|
1556
|
+
return {
|
|
1557
|
+
userId: String(claims.sub),
|
|
1558
|
+
tenantId,
|
|
1559
|
+
...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
|
|
1560
|
+
roles: Array.isArray(claims.roles) ? claims.roles.filter((r) => typeof r === "string") : [],
|
|
1561
|
+
...typeof claims.sys === "string" && claims.sys !== "" ? { system: claims.sys } : {},
|
|
1562
|
+
expiresAt: new Date(claims.exp * 1e3).toISOString(),
|
|
1563
|
+
token
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
959
1566
|
export {
|
|
960
1567
|
CauthyApiError,
|
|
961
1568
|
CauthyConfigError,
|
|
1569
|
+
CauthyTokenError,
|
|
962
1570
|
CauthyWebhookError,
|
|
1571
|
+
MIN_PASSWORD,
|
|
963
1572
|
can,
|
|
1573
|
+
checkCode,
|
|
1574
|
+
checkEmail,
|
|
1575
|
+
checkName,
|
|
1576
|
+
checkNewPassword,
|
|
1577
|
+
checkPassword,
|
|
964
1578
|
connectSystem,
|
|
965
1579
|
createCauthy,
|
|
966
1580
|
createSystem,
|
|
1581
|
+
definePolicy,
|
|
1582
|
+
policySource,
|
|
1583
|
+
pullPolicy,
|
|
967
1584
|
pushPolicy,
|
|
968
1585
|
pushTheme,
|
|
969
1586
|
themeFromStackmyth,
|
|
1587
|
+
tokensFromStackmyth,
|
|
1588
|
+
verifyToken,
|
|
970
1589
|
verifyWebhook
|
|
971
1590
|
};
|
|
972
1591
|
//# sourceMappingURL=index.mjs.map
|