cauthy-auth 0.5.1 → 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 +492 -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 +672 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +660 -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 +59 -1
- package/dist/theme.d.ts +59 -1
- package/dist/theme.js +185 -5
- package/dist/theme.js.map +1 -1
- package/dist/theme.mjs +180 -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.js
CHANGED
|
@@ -22,14 +22,26 @@ var src_exports = {};
|
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
CauthyApiError: () => CauthyApiError,
|
|
24
24
|
CauthyConfigError: () => CauthyConfigError,
|
|
25
|
+
CauthyTokenError: () => CauthyTokenError,
|
|
25
26
|
CauthyWebhookError: () => CauthyWebhookError,
|
|
27
|
+
MIN_PASSWORD: () => MIN_PASSWORD,
|
|
26
28
|
can: () => can,
|
|
29
|
+
checkCode: () => checkCode,
|
|
30
|
+
checkEmail: () => checkEmail,
|
|
31
|
+
checkName: () => checkName,
|
|
32
|
+
checkNewPassword: () => checkNewPassword,
|
|
33
|
+
checkPassword: () => checkPassword,
|
|
27
34
|
connectSystem: () => connectSystem,
|
|
28
35
|
createCauthy: () => createCauthy,
|
|
29
36
|
createSystem: () => createSystem,
|
|
37
|
+
definePolicy: () => definePolicy,
|
|
38
|
+
policySource: () => policySource,
|
|
39
|
+
pullPolicy: () => pullPolicy,
|
|
30
40
|
pushPolicy: () => pushPolicy,
|
|
31
41
|
pushTheme: () => pushTheme,
|
|
32
42
|
themeFromStackmyth: () => themeFromStackmyth,
|
|
43
|
+
tokensFromStackmyth: () => tokensFromStackmyth,
|
|
44
|
+
verifyToken: () => verifyToken,
|
|
33
45
|
verifyWebhook: () => verifyWebhook
|
|
34
46
|
});
|
|
35
47
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -145,10 +157,92 @@ function applies(d, o) {
|
|
|
145
157
|
if (o.mode === "dark" && LIGHT.test(scope)) return false;
|
|
146
158
|
const named = NAMED.exec(scope);
|
|
147
159
|
if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
|
|
148
|
-
if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
|
|
160
|
+
if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host|^\.(dark|light)\b/.test(scope)) return false;
|
|
149
161
|
}
|
|
150
162
|
return true;
|
|
151
163
|
}
|
|
164
|
+
var TOKEN_CHARS = /^[A-Za-z0-9 #%.,()/'"_+*-]+$/;
|
|
165
|
+
var TOKEN_FUNCS = /* @__PURE__ */ new Set([
|
|
166
|
+
"var",
|
|
167
|
+
"calc",
|
|
168
|
+
"min",
|
|
169
|
+
"max",
|
|
170
|
+
"clamp",
|
|
171
|
+
"rgb",
|
|
172
|
+
"rgba",
|
|
173
|
+
"hsl",
|
|
174
|
+
"hsla",
|
|
175
|
+
"hwb",
|
|
176
|
+
"lab",
|
|
177
|
+
"lch",
|
|
178
|
+
"oklab",
|
|
179
|
+
"oklch",
|
|
180
|
+
"color",
|
|
181
|
+
"color-mix",
|
|
182
|
+
"light-dark",
|
|
183
|
+
"cubic-bezier",
|
|
184
|
+
"steps",
|
|
185
|
+
"linear",
|
|
186
|
+
"linear-gradient",
|
|
187
|
+
"radial-gradient",
|
|
188
|
+
"conic-gradient",
|
|
189
|
+
"repeating-linear-gradient",
|
|
190
|
+
"repeating-radial-gradient",
|
|
191
|
+
"drop-shadow",
|
|
192
|
+
"blur",
|
|
193
|
+
"translate",
|
|
194
|
+
"translatex",
|
|
195
|
+
"translatey",
|
|
196
|
+
"scale",
|
|
197
|
+
"rotate"
|
|
198
|
+
]);
|
|
199
|
+
function tokenValue(value) {
|
|
200
|
+
const v = value.replace(/\s+/g, " ").trim();
|
|
201
|
+
if (v === "" || v.length > 400 || !TOKEN_CHARS.test(v)) return null;
|
|
202
|
+
for (const m of v.matchAll(/([A-Za-z-]+)\(/g)) if (!TOKEN_FUNCS.has(m[1].toLowerCase())) return null;
|
|
203
|
+
if (/^\(|[ ,(]\(/.test(v)) return null;
|
|
204
|
+
return v;
|
|
205
|
+
}
|
|
206
|
+
var THEME_SCOPE = /^(?:\s*(?::root|html|body|\[data-(?:theme|mode|color-scheme)(?:=["']?[\w-]+["']?)?\])\s*,?)+$/i;
|
|
207
|
+
function tokensFromStackmyth(css, options = {}) {
|
|
208
|
+
const mode = options.mode ?? "light";
|
|
209
|
+
const all = (Array.isArray(css) ? css : [css]).flatMap(declarations);
|
|
210
|
+
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);
|
|
211
|
+
const vars = /* @__PURE__ */ new Map();
|
|
212
|
+
for (const d of all) {
|
|
213
|
+
if (!applies(d, { mode, theme: named })) continue;
|
|
214
|
+
if (!d.within.every((w) => w.startsWith("@") || THEME_SCOPE.test(w))) continue;
|
|
215
|
+
vars.set(d.name, d.value);
|
|
216
|
+
}
|
|
217
|
+
const inline = (value, depth = 0) => {
|
|
218
|
+
if (depth > 16) return null;
|
|
219
|
+
let failed = false;
|
|
220
|
+
let out2 = value;
|
|
221
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
222
|
+
const next = out2.replace(/var\(\s*(--(?!sm-)[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
|
|
223
|
+
const known = vars.get(name);
|
|
224
|
+
const got = known !== void 0 ? inline(known, depth + 1) : fallback !== void 0 ? inline(fallback.trim(), depth + 1) : null;
|
|
225
|
+
if (got === null) failed = true;
|
|
226
|
+
return got ?? "";
|
|
227
|
+
});
|
|
228
|
+
if (failed) return null;
|
|
229
|
+
if (next === out2) break;
|
|
230
|
+
out2 = next;
|
|
231
|
+
}
|
|
232
|
+
return out2;
|
|
233
|
+
};
|
|
234
|
+
const tokens = {};
|
|
235
|
+
const warnings = [];
|
|
236
|
+
for (const [name, raw] of vars) {
|
|
237
|
+
if (!/^--sm-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) continue;
|
|
238
|
+
const written = inline(raw);
|
|
239
|
+
const value = written === null ? null : tokenValue(written);
|
|
240
|
+
if (value === null) warnings.push(`${name}: "${raw.length > 60 ? raw.slice(0, 57) + "\u2026" : raw}" is not a value Cauthy takes`);
|
|
241
|
+
else tokens[name] = value;
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(tokens).length === 0) warnings.push("no --sm-* token was found in these stylesheets");
|
|
244
|
+
return { tokens, warnings };
|
|
245
|
+
}
|
|
152
246
|
function resolve(value, vars, seen = 0) {
|
|
153
247
|
if (seen > 16) return null;
|
|
154
248
|
let unresolved = false;
|
|
@@ -293,7 +387,10 @@ function toWire(theme) {
|
|
|
293
387
|
panelColors: "panel_colors",
|
|
294
388
|
panelText: "panel_text",
|
|
295
389
|
panelIconUrl: "panel_icon_url",
|
|
296
|
-
texts: "texts"
|
|
390
|
+
texts: "texts",
|
|
391
|
+
inputBackground: "input_background",
|
|
392
|
+
inputBorder: "input_border",
|
|
393
|
+
tokens: "tokens"
|
|
297
394
|
};
|
|
298
395
|
const wire = {};
|
|
299
396
|
for (const [k, v] of Object.entries(theme)) {
|
|
@@ -325,18 +422,21 @@ function fromWire(wire) {
|
|
|
325
422
|
panel_colors: "panelColors",
|
|
326
423
|
panel_text: "panelText",
|
|
327
424
|
panel_icon_url: "panelIconUrl",
|
|
328
|
-
texts: "texts"
|
|
425
|
+
texts: "texts",
|
|
426
|
+
input_background: "inputBackground",
|
|
427
|
+
input_border: "inputBorder",
|
|
428
|
+
tokens: "tokens"
|
|
329
429
|
};
|
|
330
430
|
const theme = {};
|
|
331
431
|
for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
|
|
332
432
|
return theme;
|
|
333
433
|
}
|
|
334
434
|
async function pushTheme(o) {
|
|
335
|
-
const
|
|
336
|
-
const issuer = (o.issuer ??
|
|
337
|
-
const clientId = o.clientId ??
|
|
338
|
-
const tenant = o.tenant ??
|
|
339
|
-
const token = o.token ??
|
|
435
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
436
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
437
|
+
const clientId = o.clientId ?? env4("CAUTHY_CLIENT_ID") ?? "";
|
|
438
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
439
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
340
440
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
341
441
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
|
|
342
442
|
}
|
|
@@ -516,6 +616,11 @@ function createSystem(given = {}) {
|
|
|
516
616
|
return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
|
|
517
617
|
}
|
|
518
618
|
},
|
|
619
|
+
policy: {
|
|
620
|
+
async get(system) {
|
|
621
|
+
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
622
|
+
}
|
|
623
|
+
},
|
|
519
624
|
async publicJwks() {
|
|
520
625
|
if (!key) return { keys: [] };
|
|
521
626
|
const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
|
|
@@ -524,6 +629,45 @@ function createSystem(given = {}) {
|
|
|
524
629
|
};
|
|
525
630
|
}
|
|
526
631
|
|
|
632
|
+
// src/validate.ts
|
|
633
|
+
var MIN_PASSWORD = 8;
|
|
634
|
+
var MAX_PASSWORD = 256;
|
|
635
|
+
var MAX_NAME = 120;
|
|
636
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
637
|
+
function checkEmail(value) {
|
|
638
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
639
|
+
if (v === "") return { field: "email", code: "required", message: "Enter your email address." };
|
|
640
|
+
if (v.length > 254 || !EMAIL.test(v)) return { field: "email", code: "invalid_email", message: "That does not look like an email address." };
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
function checkPassword(value) {
|
|
644
|
+
if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
|
|
645
|
+
if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function checkNewPassword(value) {
|
|
649
|
+
const basic = checkPassword(value);
|
|
650
|
+
if (basic) return basic;
|
|
651
|
+
if (value.length < MIN_PASSWORD) {
|
|
652
|
+
return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
function checkName(value) {
|
|
657
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
658
|
+
if (typeof value !== "string" || value.trim().length > MAX_NAME) return { field: "name", code: "too_long", message: `Use at most ${MAX_NAME} characters.` };
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
function checkCode(value) {
|
|
662
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
663
|
+
if (v === "") return { field: "code", code: "required", message: "Enter the code." };
|
|
664
|
+
if (v.length > 40) return { field: "code", code: "too_long", message: "That is not a code." };
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
function firstProblem(...checks) {
|
|
668
|
+
return checks.find((c) => c !== null) ?? null;
|
|
669
|
+
}
|
|
670
|
+
|
|
527
671
|
// src/webhook.ts
|
|
528
672
|
var import_jose3 = require("jose");
|
|
529
673
|
var CauthyWebhookError = class extends Error {
|
|
@@ -545,8 +689,8 @@ function keysOf(issuer) {
|
|
|
545
689
|
}
|
|
546
690
|
var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
|
|
547
691
|
async function verifyWebhook(request, options = {}) {
|
|
548
|
-
const
|
|
549
|
-
const issuer = (options.issuer ??
|
|
692
|
+
const env4 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
|
|
693
|
+
const issuer = (options.issuer ?? env4 ?? "").replace(/\/+$/, "");
|
|
550
694
|
if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
|
|
551
695
|
const signature = request.headers.get("x-cauthy-signature-jwt");
|
|
552
696
|
if (!signature) throw new CauthyWebhookError("no signature");
|
|
@@ -597,9 +741,19 @@ async function verifyWebhook(request, options = {}) {
|
|
|
597
741
|
|
|
598
742
|
// src/cauthy.ts
|
|
599
743
|
var SESSION_DAYS = 30;
|
|
600
|
-
var FLOW_SECONDS =
|
|
744
|
+
var FLOW_SECONDS = 3600;
|
|
745
|
+
var AGAIN_SECONDS = 120;
|
|
601
746
|
var SKEW_SECONDS = 30;
|
|
602
747
|
var ORGANIZATIONS_TTL_MS = 6e4;
|
|
748
|
+
var LOOK_TTL_MS = 6e4;
|
|
749
|
+
var PROVIDER_ID = /^(custom:)?[a-z0-9_-]{1,40}$/;
|
|
750
|
+
function providerIn(segment) {
|
|
751
|
+
try {
|
|
752
|
+
return decodeURIComponent(segment);
|
|
753
|
+
} catch {
|
|
754
|
+
return "";
|
|
755
|
+
}
|
|
756
|
+
}
|
|
603
757
|
function env2(name) {
|
|
604
758
|
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
605
759
|
}
|
|
@@ -632,6 +786,7 @@ function createCauthy(given = {}) {
|
|
|
632
786
|
const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
|
|
633
787
|
const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
|
|
634
788
|
const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
|
|
789
|
+
const tenant = given.tenant ?? env2("CAUTHY_TENANT") ?? "";
|
|
635
790
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
|
|
636
791
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
637
792
|
if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
|
|
@@ -640,18 +795,26 @@ function createCauthy(given = {}) {
|
|
|
640
795
|
const afterSignOut = localPath(given.afterSignOut, "/");
|
|
641
796
|
const cookieName = given.cookieName ?? "cauthy_session";
|
|
642
797
|
const flowCookie = `${cookieName}_flow`;
|
|
798
|
+
const againCookie = `${cookieName}_again`;
|
|
799
|
+
const stepCookie = `${cookieName}_step`;
|
|
643
800
|
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
644
801
|
const key = keyOf(secret);
|
|
802
|
+
const pages = {
|
|
803
|
+
signIn: given.pages?.signIn ? localPath(given.pages.signIn, "/") : void 0,
|
|
804
|
+
resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
|
|
805
|
+
verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
|
|
806
|
+
};
|
|
807
|
+
const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
|
|
645
808
|
const originOf = (request) => (given.baseURL ?? new URL(request.url).origin).replace(/\/+$/, "");
|
|
646
809
|
const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
|
|
647
810
|
async function token(form) {
|
|
648
|
-
const
|
|
649
|
-
if (clientSecret)
|
|
811
|
+
const body2 = new URLSearchParams({ client_id: clientId, ...form });
|
|
812
|
+
if (clientSecret) body2.set("client_secret", clientSecret);
|
|
650
813
|
try {
|
|
651
814
|
const res = await doFetch(`${issuer}/v1/oauth/token`, {
|
|
652
815
|
method: "POST",
|
|
653
816
|
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
654
|
-
body
|
|
817
|
+
body: body2
|
|
655
818
|
});
|
|
656
819
|
if (res.ok) return { ok: true, body: await res.json() };
|
|
657
820
|
return { ok: false, ended: res.status >= 400 && res.status < 500 };
|
|
@@ -681,12 +844,12 @@ function createCauthy(given = {}) {
|
|
|
681
844
|
return null;
|
|
682
845
|
}
|
|
683
846
|
}
|
|
684
|
-
async function stored(
|
|
685
|
-
if (typeof
|
|
686
|
-
const who = await userinfo(
|
|
847
|
+
async function stored(body2) {
|
|
848
|
+
if (typeof body2?.access_token !== "string" || typeof body2?.refresh_token !== "string") return null;
|
|
849
|
+
const who = await userinfo(body2.access_token);
|
|
687
850
|
if (who === null || who.u.id === "") return null;
|
|
688
|
-
const lifetime = typeof
|
|
689
|
-
return { at:
|
|
851
|
+
const lifetime = typeof body2.expires_in === "number" && body2.expires_in > 0 ? body2.expires_in : 300;
|
|
852
|
+
return { at: body2.access_token, rt: body2.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who };
|
|
690
853
|
}
|
|
691
854
|
function toSession(s) {
|
|
692
855
|
return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1e3).toISOString() };
|
|
@@ -760,8 +923,8 @@ function createCauthy(given = {}) {
|
|
|
760
923
|
const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
|
|
761
924
|
const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
|
|
762
925
|
if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`);
|
|
763
|
-
const
|
|
764
|
-
const value = (
|
|
926
|
+
const body2 = await res.json();
|
|
927
|
+
const value = (body2.data ?? []).map(
|
|
765
928
|
(o) => ({
|
|
766
929
|
id: String(o.id),
|
|
767
930
|
system: String(o.system ?? ""),
|
|
@@ -806,20 +969,28 @@ function createCauthy(given = {}) {
|
|
|
806
969
|
if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
|
|
807
970
|
const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
|
|
808
971
|
if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
|
|
809
|
-
|
|
972
|
+
const passkey = url.searchParams.get("method") === "passkey" ? "#passkey" : "";
|
|
973
|
+
return redirect(`${issuer}/v1/oauth/authorize?${q}${passkey}`, [
|
|
810
974
|
writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS))
|
|
811
975
|
]);
|
|
812
976
|
}
|
|
813
977
|
function failed(request, reason) {
|
|
814
978
|
const h = new Headers({ "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
815
979
|
h.append("set-cookie", writeCookie(flowCookie, "", attrs(request, 0)));
|
|
980
|
+
h.append("set-cookie", writeCookie(againCookie, "", attrs(request, 0)));
|
|
816
981
|
return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h });
|
|
817
982
|
}
|
|
818
983
|
async function callback(request, url) {
|
|
819
984
|
const had = readCookies(request.headers.get("cookie"));
|
|
820
985
|
const sealed = had.get(flowCookie);
|
|
821
986
|
const flow = sealed ? await unseal(sealed, await key) : null;
|
|
822
|
-
if (flow === null || url.searchParams.get("state") !== flow.state)
|
|
987
|
+
if (flow === null || url.searchParams.get("state") !== flow.state) {
|
|
988
|
+
if (had.get(againCookie) === void 0) {
|
|
989
|
+
const back = flow?.returnTo ?? afterSignIn;
|
|
990
|
+
return redirect(`${basePath}/login?return_to=${encodeURIComponent(back)}`, [writeCookie(againCookie, "1", attrs(request, AGAIN_SECONDS))]);
|
|
991
|
+
}
|
|
992
|
+
return failed(request, "state_mismatch");
|
|
993
|
+
}
|
|
823
994
|
const error = url.searchParams.get("error");
|
|
824
995
|
if (error !== null) {
|
|
825
996
|
if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
@@ -835,28 +1006,332 @@ function createCauthy(given = {}) {
|
|
|
835
1006
|
});
|
|
836
1007
|
const s = res.ok ? await stored(res.body) : null;
|
|
837
1008
|
if (s === null) return failed(request, "exchange_failed");
|
|
838
|
-
return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
1009
|
+
return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]);
|
|
839
1010
|
}
|
|
840
1011
|
async function logout(request) {
|
|
841
1012
|
const { had, value } = await read(request);
|
|
842
1013
|
if (value !== null) {
|
|
843
|
-
const
|
|
844
|
-
if (clientSecret)
|
|
1014
|
+
const body2 = new URLSearchParams({ client_id: clientId, token: value.rt });
|
|
1015
|
+
if (clientSecret) body2.set("client_secret", clientSecret);
|
|
845
1016
|
await doFetch(`${issuer}/v1/oauth/revoke`, {
|
|
846
1017
|
method: "POST",
|
|
847
1018
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
848
|
-
body
|
|
1019
|
+
body: body2
|
|
849
1020
|
}).catch(() => void 0);
|
|
850
1021
|
orgCache.delete(value.at);
|
|
851
1022
|
}
|
|
852
1023
|
const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
|
|
853
1024
|
return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
|
|
854
1025
|
}
|
|
1026
|
+
const STEP_SECONDS = 600;
|
|
1027
|
+
const problem = (status2, code, message, field, headers) => json({ error: { code, message, ...field ? { field } : {} } }, status2, headers);
|
|
1028
|
+
const invalid = (p) => problem(400, p.code, p.message, p.field);
|
|
1029
|
+
function ownRequest(request) {
|
|
1030
|
+
const origin = request.headers.get("origin");
|
|
1031
|
+
if (origin !== null) return origin === originOf(request) || origin === new URL(request.url).origin;
|
|
1032
|
+
return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
|
|
1033
|
+
}
|
|
1034
|
+
async function body(request) {
|
|
1035
|
+
if (!(request.headers.get("content-type") ?? "").includes("application/json")) return null;
|
|
1036
|
+
try {
|
|
1037
|
+
const b = await request.json();
|
|
1038
|
+
return typeof b === "object" && b !== null && !Array.isArray(b) ? b : null;
|
|
1039
|
+
} catch {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const clientIp = given.clientIp ?? ((request) => request.headers.get("x-forwarded-for")?.split(",").pop()?.trim() || void 0);
|
|
1044
|
+
let noProofUntil = 0;
|
|
1045
|
+
async function passedOn(request) {
|
|
1046
|
+
const ip = clientIp(request);
|
|
1047
|
+
if (!ip || Date.now() < noProofUntil) return {};
|
|
1048
|
+
try {
|
|
1049
|
+
return { "x-cauthy-client-ip": ip, "x-cauthy-system-token": await ownSystem.token() };
|
|
1050
|
+
} catch {
|
|
1051
|
+
noProofUntil = Date.now() + 5 * 6e4;
|
|
1052
|
+
return {};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
async function ask(request, method, path, payload, page, bearer2) {
|
|
1056
|
+
try {
|
|
1057
|
+
const res = await doFetch(`${issuer}${path}`, {
|
|
1058
|
+
method,
|
|
1059
|
+
headers: {
|
|
1060
|
+
"x-tenant-key": tenant,
|
|
1061
|
+
accept: "application/json",
|
|
1062
|
+
// A link Cauthy mails opens the application's page for it when it
|
|
1063
|
+
// has one; otherwise Cauthy's own. Either way in the reader's language.
|
|
1064
|
+
...page ? { "x-cauthy-link-to": `${originOf(request)}${page}`, "x-cauthy-client-id": clientId } : { "x-cauthy-links": "hosted" },
|
|
1065
|
+
...await passedOn(request),
|
|
1066
|
+
...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
|
|
1067
|
+
...request.headers.get("accept-language") ? { "accept-language": request.headers.get("accept-language") } : {},
|
|
1068
|
+
...request.headers.get("user-agent") ? { "user-agent": request.headers.get("user-agent") } : {},
|
|
1069
|
+
...payload === void 0 ? {} : { "content-type": "application/json" }
|
|
1070
|
+
},
|
|
1071
|
+
body: payload === void 0 ? void 0 : JSON.stringify(payload)
|
|
1072
|
+
});
|
|
1073
|
+
const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
|
|
1074
|
+
if (res.ok) return { status: res.status, data: b?.data ?? b };
|
|
1075
|
+
const first = b?.errors?.[0];
|
|
1076
|
+
return { status: res.status, data: null, error: { code: String(first?.code ?? "error"), message: String(first?.message ?? "The request could not be completed.") } };
|
|
1077
|
+
} catch {
|
|
1078
|
+
return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const accountOf = (d) => ({
|
|
1082
|
+
id: String(d?.id ?? ""),
|
|
1083
|
+
email: String(d?.email ?? ""),
|
|
1084
|
+
name: String(d?.name ?? ""),
|
|
1085
|
+
emailVerified: d?.email_verified === true,
|
|
1086
|
+
hasPassword: d?.has_password === true,
|
|
1087
|
+
mfaEnabled: d?.mfa_enabled === true
|
|
1088
|
+
});
|
|
1089
|
+
async function signedInAs(request) {
|
|
1090
|
+
const r = await resolve2(request);
|
|
1091
|
+
const headers = new Headers();
|
|
1092
|
+
for (const c of r.cookies) headers.append("set-cookie", c);
|
|
1093
|
+
if (r.session === null) return { response: problem(401, "unauthenticated", "Sign in first.", void 0, headers) };
|
|
1094
|
+
return { token: r.session.accessToken, kept: r.cookies.length === 0 ? (await read(request)).value : null, headers };
|
|
1095
|
+
}
|
|
1096
|
+
async function signedIn(request, tokens, extra = []) {
|
|
1097
|
+
const s = await stored(tokens);
|
|
1098
|
+
if (s === null) return problem(502, "unavailable", "Sign-in is unavailable right now. Try again in a moment.");
|
|
1099
|
+
const h = new Headers();
|
|
1100
|
+
for (const c of [...await write(request, s, readCookies(request.headers.get("cookie"))), ...extra]) h.append("set-cookie", c);
|
|
1101
|
+
return json({ status: "signed_in", session: await toPublic(toSession(s)) }, 200, h);
|
|
1102
|
+
}
|
|
1103
|
+
let lookCache = null;
|
|
1104
|
+
async function ownRoute(request, url, route) {
|
|
1105
|
+
const post = request.method === "POST";
|
|
1106
|
+
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/");
|
|
1107
|
+
if (!own) return null;
|
|
1108
|
+
if (tenant === "") {
|
|
1109
|
+
return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
|
|
1110
|
+
}
|
|
1111
|
+
if (route === "/look" && !post) {
|
|
1112
|
+
if (lookCache === null || lookCache.until < Date.now()) {
|
|
1113
|
+
const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(clientId)}`);
|
|
1114
|
+
lookCache = { until: Date.now() + LOOK_TTL_MS, look: res.error ? {} : res.data ?? {} };
|
|
1115
|
+
}
|
|
1116
|
+
return json(lookCache.look);
|
|
1117
|
+
}
|
|
1118
|
+
if (route === "/methods" && !post) {
|
|
1119
|
+
const [providers, settings] = await Promise.all([ask(request, "GET", "/v1/auth/social/providers"), ask(request, "GET", "/v1/auth/settings")]);
|
|
1120
|
+
const list = Array.isArray(providers.data) ? providers.data : [];
|
|
1121
|
+
return json({
|
|
1122
|
+
providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
|
|
1123
|
+
signUp: settings.data?.allow_signup !== false,
|
|
1124
|
+
// A sign-in link needs a page of the application to open.
|
|
1125
|
+
magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
|
|
1126
|
+
// Asked for on Cauthy's page: the person goes there for that step and comes back.
|
|
1127
|
+
passkey: true
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
if (route.startsWith("/sign-in/social/") && !post) {
|
|
1131
|
+
const provider = providerIn(route.slice("/sign-in/social/".length));
|
|
1132
|
+
if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
|
|
1133
|
+
const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
1134
|
+
const callback2 = `${basePath}/callback/${provider}`;
|
|
1135
|
+
const started = await ask(request, "GET", `/v1/auth/social/${provider}?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2);
|
|
1136
|
+
if (started.error || typeof started.data?.redirect_url !== "string") {
|
|
1137
|
+
return redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(started.error?.code ?? "provider_unavailable")}`);
|
|
1138
|
+
}
|
|
1139
|
+
const step = { kind: "social", token: String(started.data.state ?? ""), provider, returnTo };
|
|
1140
|
+
return redirect(started.data.redirect_url, [writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS))]);
|
|
1141
|
+
}
|
|
1142
|
+
if (route.startsWith("/callback/") && !post) {
|
|
1143
|
+
const provider = providerIn(route.slice("/callback/".length));
|
|
1144
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1145
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1146
|
+
const clear = writeCookie(stepCookie, "", attrs(request, 0));
|
|
1147
|
+
const back = step?.returnTo ?? afterSignIn;
|
|
1148
|
+
const failedTo = (code2) => redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code2)}`, [clear]);
|
|
1149
|
+
if (step === null || step.kind !== "social" || step.provider !== provider || url.searchParams.get("state") !== step.token) return failedTo("state_mismatch");
|
|
1150
|
+
const code = url.searchParams.get("code");
|
|
1151
|
+
if (!code) return failedTo(url.searchParams.get("error") === "access_denied" ? "cancelled" : "provider_refused");
|
|
1152
|
+
const q = new URLSearchParams({ code, state: step.token, redirect_uri: `${originOf(request)}${basePath}/callback/${provider}` });
|
|
1153
|
+
const done = await ask(request, "GET", `/v1/auth/social/${provider}/callback?${q}`);
|
|
1154
|
+
if (done.error) return failedTo(done.error.code);
|
|
1155
|
+
const s = await stored(done.data);
|
|
1156
|
+
if (s === null) return failedTo("unavailable");
|
|
1157
|
+
return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
|
|
1158
|
+
}
|
|
1159
|
+
if ((route === "/me" || route === "/me/sessions") && !post) {
|
|
1160
|
+
const mine = await signedInAs(request);
|
|
1161
|
+
if ("response" in mine) return mine.response;
|
|
1162
|
+
const res = await ask(request, "GET", route === "/me" ? "/v1/auth/me" : "/v1/auth/me/sessions", void 0, void 0, mine.token);
|
|
1163
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
1164
|
+
if (route === "/me/sessions") {
|
|
1165
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
1166
|
+
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);
|
|
1167
|
+
}
|
|
1168
|
+
return json({ account: accountOf(res.data) }, 200, mine.headers);
|
|
1169
|
+
}
|
|
1170
|
+
if (!post) return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
1171
|
+
if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
|
|
1172
|
+
if (route === "/sign-out") {
|
|
1173
|
+
const { had, value } = await read(request);
|
|
1174
|
+
if (value !== null) {
|
|
1175
|
+
const form = new URLSearchParams({ client_id: clientId, token: value.rt });
|
|
1176
|
+
if (clientSecret) form.set("client_secret", clientSecret);
|
|
1177
|
+
await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
|
|
1178
|
+
orgCache.delete(value.at);
|
|
1179
|
+
}
|
|
1180
|
+
const h = new Headers();
|
|
1181
|
+
for (const c of clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
|
|
1182
|
+
return json({ status: "signed_out" }, 200, h);
|
|
1183
|
+
}
|
|
1184
|
+
const b = await body(request);
|
|
1185
|
+
if (b === null) return problem(400, "bad_request", "Send the form as JSON.");
|
|
1186
|
+
if (route === "/sign-in/email") {
|
|
1187
|
+
const bad = firstProblem(checkEmail(b.email), checkPassword(b.password));
|
|
1188
|
+
if (bad) return invalid(bad);
|
|
1189
|
+
const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password });
|
|
1190
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1191
|
+
if (res.data?.mfa_required === true) {
|
|
1192
|
+
const step = { kind: "mfa", token: String(res.data.mfa_token ?? ""), returnTo: afterSignIn };
|
|
1193
|
+
return json({ status: "mfa_required" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
|
|
1194
|
+
}
|
|
1195
|
+
return signedIn(request, res.data);
|
|
1196
|
+
}
|
|
1197
|
+
if (route === "/sign-in/mfa") {
|
|
1198
|
+
const bad = checkCode(b.code);
|
|
1199
|
+
if (bad) return invalid(bad);
|
|
1200
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1201
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1202
|
+
if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
|
|
1203
|
+
const res = await ask(request, "POST", "/v1/auth/mfa/challenge", { mfa_token: step.token, code: String(b.code).trim() });
|
|
1204
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
|
|
1205
|
+
return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
|
|
1206
|
+
}
|
|
1207
|
+
if (route === "/sign-up/email") {
|
|
1208
|
+
const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password));
|
|
1209
|
+
if (bad) return invalid(bad);
|
|
1210
|
+
const email = String(b.email).trim();
|
|
1211
|
+
const made = await ask(request, "POST", "/v1/auth/register", { email, password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "" }, pages.verifyEmail);
|
|
1212
|
+
if (made.error) return problem(made.status, made.error.code, made.error.message);
|
|
1213
|
+
if (made.data?.verification_required === true) return json({ status: "verification_required", email });
|
|
1214
|
+
const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
|
|
1215
|
+
if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
|
|
1216
|
+
return signedIn(request, res.data);
|
|
1217
|
+
}
|
|
1218
|
+
if (route === "/forgot-password") {
|
|
1219
|
+
const bad = checkEmail(b.email);
|
|
1220
|
+
if (bad) return invalid(bad);
|
|
1221
|
+
await ask(request, "POST", "/v1/auth/password/reset", { email: String(b.email).trim() }, pages.resetPassword);
|
|
1222
|
+
return json({ status: "sent" });
|
|
1223
|
+
}
|
|
1224
|
+
if (route.startsWith("/me")) {
|
|
1225
|
+
const mine = await signedInAs(request);
|
|
1226
|
+
if ("response" in mine) return mine.response;
|
|
1227
|
+
const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
|
|
1228
|
+
const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers);
|
|
1229
|
+
const password = typeof b.password === "string" ? b.password : "";
|
|
1230
|
+
if (route === "/me") {
|
|
1231
|
+
const bad = checkName(b.name);
|
|
1232
|
+
if (bad) return invalid(bad);
|
|
1233
|
+
const res = await as("/v1/auth/me", { name: typeof b.name === "string" ? b.name.trim() : "" }, "PATCH");
|
|
1234
|
+
if (res.error) return refused(res, "name");
|
|
1235
|
+
const h = mine.headers;
|
|
1236
|
+
if (mine.kept !== null) {
|
|
1237
|
+
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)) });
|
|
1238
|
+
if (again !== null) for (const c of await write(request, again, readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
|
|
1239
|
+
}
|
|
1240
|
+
return json({ status: "saved", account: accountOf(res.data) }, 200, h);
|
|
1241
|
+
}
|
|
1242
|
+
if (route === "/me/password") {
|
|
1243
|
+
const bad = checkNewPassword(b.newPassword);
|
|
1244
|
+
if (bad) return problem(400, bad.code, bad.message, "newPassword");
|
|
1245
|
+
const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
|
|
1246
|
+
if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
|
|
1247
|
+
const next = await stored(res.data);
|
|
1248
|
+
const h = new Headers();
|
|
1249
|
+
const had = readCookies(request.headers.get("cookie"));
|
|
1250
|
+
for (const c of next !== null ? await write(request, next, had) : clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
|
|
1251
|
+
return json({ status: "changed", signedIn: next !== null }, 200, h);
|
|
1252
|
+
}
|
|
1253
|
+
if (route === "/me/mfa/setup") {
|
|
1254
|
+
const res = await as("/v1/auth/mfa/setup", { password });
|
|
1255
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
1256
|
+
return json({ status: "started", secret: String(res.data?.secret ?? ""), otpauthUri: String(res.data?.otpauth_uri ?? "") }, 200, mine.headers);
|
|
1257
|
+
}
|
|
1258
|
+
if (route === "/me/mfa/verify") {
|
|
1259
|
+
const bad = checkCode(b.code);
|
|
1260
|
+
if (bad) return invalid(bad);
|
|
1261
|
+
const res = await as("/v1/auth/mfa/verify", { code: String(b.code).trim() });
|
|
1262
|
+
if (res.error) return refused(res, "code");
|
|
1263
|
+
return json({ status: "enabled", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
|
|
1264
|
+
}
|
|
1265
|
+
if (route === "/me/mfa/disable") {
|
|
1266
|
+
const res = await as("/v1/auth/me/mfa/disable", { password });
|
|
1267
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
1268
|
+
return json({ status: "disabled" }, 200, mine.headers);
|
|
1269
|
+
}
|
|
1270
|
+
if (route === "/me/mfa/recovery-codes") {
|
|
1271
|
+
const res = await as("/v1/auth/me/mfa/recovery-codes", { password });
|
|
1272
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
1273
|
+
return json({ status: "renewed", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
|
|
1274
|
+
}
|
|
1275
|
+
if (route === "/me/sessions/end") {
|
|
1276
|
+
const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
|
|
1277
|
+
if (res.error) return refused(res);
|
|
1278
|
+
const h = new Headers();
|
|
1279
|
+
for (const c of clearParts(cookieName, attrs(request, 0), readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
|
|
1280
|
+
orgCache.delete(mine.token);
|
|
1281
|
+
return json({ status: "signed_out" }, 200, h);
|
|
1282
|
+
}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
const isToken = (v) => typeof v === "string" && v !== "" && v.length <= 512;
|
|
1286
|
+
const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
|
|
1287
|
+
if (route === "/reset-password") {
|
|
1288
|
+
if (!isToken(b.token)) return badLink();
|
|
1289
|
+
const bad = checkNewPassword(b.password);
|
|
1290
|
+
if (bad) return invalid(bad);
|
|
1291
|
+
const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
|
|
1292
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0);
|
|
1293
|
+
return json({ status: "changed" });
|
|
1294
|
+
}
|
|
1295
|
+
if (route === "/verify-email") {
|
|
1296
|
+
if (!isToken(b.token)) return badLink();
|
|
1297
|
+
const res = await ask(request, "POST", "/v1/auth/email/verify", { token: String(b.token) });
|
|
1298
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1299
|
+
return json({ status: "verified" });
|
|
1300
|
+
}
|
|
1301
|
+
if (route === "/sign-in/magic-link") {
|
|
1302
|
+
const bad = checkEmail(b.email);
|
|
1303
|
+
if (bad) return invalid(bad);
|
|
1304
|
+
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");
|
|
1305
|
+
const email = String(b.email).trim();
|
|
1306
|
+
await ask(request, "POST", "/v1/auth/magic-link/request", { email }, pages.signIn);
|
|
1307
|
+
const step = { kind: "code", token: email, returnTo: afterSignIn };
|
|
1308
|
+
return json({ status: "sent" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
|
|
1309
|
+
}
|
|
1310
|
+
if (route === "/sign-in/email-code") {
|
|
1311
|
+
const bad = checkCode(b.code);
|
|
1312
|
+
if (bad) return invalid(bad);
|
|
1313
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1314
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1315
|
+
if (step === null || step.kind !== "code") return problem(400, "step_expired", "Ask for a new link: this one was asked for too long ago.");
|
|
1316
|
+
const res = await ask(request, "POST", "/v1/auth/magic-link/verify-code", { email: step.token, code: String(b.code).trim() });
|
|
1317
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
|
|
1318
|
+
return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
|
|
1319
|
+
}
|
|
1320
|
+
if (route === "/sign-in/magic-link/verify") {
|
|
1321
|
+
if (!isToken(b.token)) return badLink();
|
|
1322
|
+
const res = await ask(request, "POST", "/v1/auth/magic-link/verify", { token: String(b.token) });
|
|
1323
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1324
|
+
return signedIn(request, res.data);
|
|
1325
|
+
}
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
855
1328
|
async function handler(request) {
|
|
856
1329
|
const url = new URL(request.url);
|
|
857
1330
|
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
|
|
858
1331
|
const route = url.pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
|
|
859
1332
|
const get = request.method === "GET" || request.method === "HEAD";
|
|
1333
|
+
const mine = await ownRoute(request, url, route);
|
|
1334
|
+
if (mine !== null) return mine;
|
|
860
1335
|
if (route === "/login" && get) return login(request, url);
|
|
861
1336
|
if (route === "/callback" && get) return callback(request, url);
|
|
862
1337
|
if (route === "/logout" && (get || request.method === "POST")) return logout(request);
|
|
@@ -900,7 +1375,7 @@ function createCauthy(given = {}) {
|
|
|
900
1375
|
return {
|
|
901
1376
|
handler,
|
|
902
1377
|
webhook,
|
|
903
|
-
system:
|
|
1378
|
+
system: ownSystem,
|
|
904
1379
|
getSession,
|
|
905
1380
|
status,
|
|
906
1381
|
resolve: resolve2,
|
|
@@ -925,12 +1400,101 @@ function can(organization, resource, action) {
|
|
|
925
1400
|
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
926
1401
|
return organization.permissions.some((p) => matches(p, resource, action));
|
|
927
1402
|
}
|
|
1403
|
+
function definePolicy(roles) {
|
|
1404
|
+
const entry = (p) => `${p.resource}:${p.action}`;
|
|
1405
|
+
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
1406
|
+
const split = (permission) => {
|
|
1407
|
+
const i = permission.lastIndexOf(":");
|
|
1408
|
+
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
1409
|
+
};
|
|
1410
|
+
return {
|
|
1411
|
+
roles: roles.map((r) => ({
|
|
1412
|
+
role: r.role,
|
|
1413
|
+
...r.name !== void 0 ? { name: r.name } : {},
|
|
1414
|
+
...r.description !== void 0 ? { description: r.description } : {},
|
|
1415
|
+
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
1416
|
+
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
1417
|
+
})),
|
|
1418
|
+
roleNames: roles.map((r) => r.role),
|
|
1419
|
+
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
1420
|
+
can(organization, permission) {
|
|
1421
|
+
const [resource, action] = split(permission);
|
|
1422
|
+
return can(organization, resource, action);
|
|
1423
|
+
},
|
|
1424
|
+
hasRole(organization, ...wanted) {
|
|
1425
|
+
return organization != null && wanted.includes(organization.role);
|
|
1426
|
+
},
|
|
1427
|
+
permissionsOf(role) {
|
|
1428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1429
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
1430
|
+
const walk = (name) => {
|
|
1431
|
+
if (seen.has(name)) return;
|
|
1432
|
+
seen.add(name);
|
|
1433
|
+
const r = byName.get(name);
|
|
1434
|
+
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
1435
|
+
for (const parent of r?.inherits ?? []) walk(parent);
|
|
1436
|
+
};
|
|
1437
|
+
walk(role);
|
|
1438
|
+
return [...out2];
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1443
|
+
function policySource(system, roles) {
|
|
1444
|
+
const text = (v) => JSON.stringify(v);
|
|
1445
|
+
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
1446
|
+
const one = (r) => {
|
|
1447
|
+
const parts = [`role: ${text(r.role)}`];
|
|
1448
|
+
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
1449
|
+
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
1450
|
+
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
1451
|
+
if (r.permissions?.length) {
|
|
1452
|
+
const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
|
|
1453
|
+
parts.push(`permissions: [
|
|
1454
|
+
${list.join("\n")}
|
|
1455
|
+
]`);
|
|
1456
|
+
}
|
|
1457
|
+
return ` {
|
|
1458
|
+
${parts.join(",\n ")},
|
|
1459
|
+
},`;
|
|
1460
|
+
};
|
|
1461
|
+
return `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
1462
|
+
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
1463
|
+
// changes, and do not edit this file by hand.
|
|
1464
|
+
|
|
1465
|
+
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
1466
|
+
|
|
1467
|
+
export const policy = definePolicy([
|
|
1468
|
+
${roles.map(one).join("\n")}
|
|
1469
|
+
])
|
|
1470
|
+
|
|
1471
|
+
export const { can, hasRole } = policy
|
|
1472
|
+
export type Role = RoleOf<typeof policy>
|
|
1473
|
+
export type Permission = PermissionOf<typeof policy>
|
|
1474
|
+
`;
|
|
1475
|
+
}
|
|
1476
|
+
async function pullPolicy(o = {}) {
|
|
1477
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
1478
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1479
|
+
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
1480
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
1481
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
1482
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
1483
|
+
if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
|
|
1484
|
+
}
|
|
1485
|
+
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
1486
|
+
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
|
|
1487
|
+
});
|
|
1488
|
+
const body = await res.json().catch(() => null);
|
|
1489
|
+
if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
1490
|
+
return body?.data?.roles ?? [];
|
|
1491
|
+
}
|
|
928
1492
|
async function pushPolicy(o) {
|
|
929
|
-
const
|
|
930
|
-
const issuer = (o.issuer ??
|
|
931
|
-
const system = o.system ??
|
|
932
|
-
const tenant = o.tenant ??
|
|
933
|
-
const token = o.token ??
|
|
1493
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
1494
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1495
|
+
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
1496
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
1497
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
934
1498
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
935
1499
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
|
|
936
1500
|
}
|
|
@@ -953,10 +1517,10 @@ function sessionSecret() {
|
|
|
953
1517
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
954
1518
|
}
|
|
955
1519
|
async function connectSystem(o) {
|
|
956
|
-
const
|
|
957
|
-
const issuer = (o.issuer ??
|
|
958
|
-
const tenant = o.tenant ??
|
|
959
|
-
const token = o.token ??
|
|
1520
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
1521
|
+
const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1522
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
1523
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
960
1524
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
961
1525
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
|
|
962
1526
|
}
|
|
@@ -988,18 +1552,89 @@ async function connectSystem(o) {
|
|
|
988
1552
|
env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
|
|
989
1553
|
};
|
|
990
1554
|
}
|
|
1555
|
+
|
|
1556
|
+
// src/verify.ts
|
|
1557
|
+
var import_jose4 = require("jose");
|
|
1558
|
+
var CauthyTokenError = class extends Error {
|
|
1559
|
+
constructor(reason) {
|
|
1560
|
+
super(`cauthy-auth: token refused: ${reason}`);
|
|
1561
|
+
this.reason = reason;
|
|
1562
|
+
this.name = "CauthyTokenError";
|
|
1563
|
+
}
|
|
1564
|
+
reason;
|
|
1565
|
+
};
|
|
1566
|
+
var keys2 = /* @__PURE__ */ new Map();
|
|
1567
|
+
function keysOf2(issuer) {
|
|
1568
|
+
let set = keys2.get(issuer);
|
|
1569
|
+
if (set === void 0) {
|
|
1570
|
+
set = (0, import_jose4.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
|
|
1571
|
+
keys2.set(issuer, set);
|
|
1572
|
+
}
|
|
1573
|
+
return set;
|
|
1574
|
+
}
|
|
1575
|
+
function env3(name) {
|
|
1576
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
1577
|
+
}
|
|
1578
|
+
function bearer(from) {
|
|
1579
|
+
if (typeof from === "string") return from.replace(/^Bearer\s+/i, "").trim();
|
|
1580
|
+
const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
|
|
1581
|
+
return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
|
|
1582
|
+
}
|
|
1583
|
+
async function verifyToken(from, options = {}) {
|
|
1584
|
+
const issuer = (options.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
1585
|
+
if (issuer === "") throw new CauthyTokenError("issuer (CAUTHY_ISSUER) is not set");
|
|
1586
|
+
const token = bearer(from);
|
|
1587
|
+
if (token === "") throw new CauthyTokenError("no token");
|
|
1588
|
+
let claims;
|
|
1589
|
+
try {
|
|
1590
|
+
const verified = await (0, import_jose4.jwtVerify)(token, options.jwks ?? keysOf2(issuer), {
|
|
1591
|
+
algorithms: ["RS256"],
|
|
1592
|
+
issuer,
|
|
1593
|
+
audience: options.audience ?? env3("CAUTHY_AUDIENCE"),
|
|
1594
|
+
requiredClaims: ["exp", "sub"],
|
|
1595
|
+
currentDate: options.now
|
|
1596
|
+
});
|
|
1597
|
+
claims = verified.payload;
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
throw new CauthyTokenError(err instanceof Error ? err.message : "bad signature");
|
|
1600
|
+
}
|
|
1601
|
+
const tenantId = typeof claims.tid === "string" ? claims.tid : "";
|
|
1602
|
+
const wanted = options.tenantId ?? env3("CAUTHY_TENANT_ID");
|
|
1603
|
+
if (wanted && tenantId !== wanted) throw new CauthyTokenError("issued for another workspace");
|
|
1604
|
+
return {
|
|
1605
|
+
userId: String(claims.sub),
|
|
1606
|
+
tenantId,
|
|
1607
|
+
...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
|
|
1608
|
+
roles: Array.isArray(claims.roles) ? claims.roles.filter((r) => typeof r === "string") : [],
|
|
1609
|
+
...typeof claims.sys === "string" && claims.sys !== "" ? { system: claims.sys } : {},
|
|
1610
|
+
expiresAt: new Date(claims.exp * 1e3).toISOString(),
|
|
1611
|
+
token
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
991
1614
|
// Annotate the CommonJS export names for ESM import in node:
|
|
992
1615
|
0 && (module.exports = {
|
|
993
1616
|
CauthyApiError,
|
|
994
1617
|
CauthyConfigError,
|
|
1618
|
+
CauthyTokenError,
|
|
995
1619
|
CauthyWebhookError,
|
|
1620
|
+
MIN_PASSWORD,
|
|
996
1621
|
can,
|
|
1622
|
+
checkCode,
|
|
1623
|
+
checkEmail,
|
|
1624
|
+
checkName,
|
|
1625
|
+
checkNewPassword,
|
|
1626
|
+
checkPassword,
|
|
997
1627
|
connectSystem,
|
|
998
1628
|
createCauthy,
|
|
999
1629
|
createSystem,
|
|
1630
|
+
definePolicy,
|
|
1631
|
+
policySource,
|
|
1632
|
+
pullPolicy,
|
|
1000
1633
|
pushPolicy,
|
|
1001
1634
|
pushTheme,
|
|
1002
1635
|
themeFromStackmyth,
|
|
1636
|
+
tokensFromStackmyth,
|
|
1637
|
+
verifyToken,
|
|
1003
1638
|
verifyWebhook
|
|
1004
1639
|
});
|
|
1005
1640
|
//# sourceMappingURL=index.js.map
|