@cauthyhq/node 0.0.0 → 1.1.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/CHANGELOG.md +43 -0
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +184 -1
- package/dist/cauthy-Bn-i7cKD.d.mts +1058 -0
- package/dist/cauthy-Wy-YDGrM.d.ts +1058 -0
- package/dist/express.d.mts +25 -0
- package/dist/express.d.ts +25 -0
- package/dist/express.js +103 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +77 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +38 -0
- package/dist/fastify.d.ts +38 -0
- package/dist/fastify.js +105 -0
- package/dist/fastify.js.map +1 -0
- package/dist/fastify.mjs +79 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +24 -0
- package/dist/hono.d.ts +24 -0
- package/dist/hono.js +51 -0
- package/dist/hono.js.map +1 -0
- package/dist/hono.mjs +25 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +3485 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3421 -0
- package/dist/index.mjs.map +1 -0
- package/dist/keys-D5k0omTb.d.mts +64 -0
- package/dist/keys-D5k0omTb.d.ts +64 -0
- package/dist/keys.d.mts +8 -0
- package/dist/keys.d.ts +8 -0
- package/dist/keys.js +157 -0
- package/dist/keys.js.map +1 -0
- package/dist/keys.mjs +129 -0
- package/dist/keys.mjs.map +1 -0
- package/dist/mcp.d.mts +151 -0
- package/dist/mcp.d.ts +151 -0
- package/dist/mcp.js +410 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mcp.mjs +382 -0
- package/dist/mcp.mjs.map +1 -0
- 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/testing.d.mts +74 -0
- package/dist/testing.d.ts +74 -0
- package/dist/testing.js +270 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +243 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/theme.d.mts +183 -0
- package/dist/theme.d.ts +183 -0
- package/dist/theme.js +531 -0
- package/dist/theme.js.map +1 -0
- package/dist/theme.mjs +496 -0
- package/dist/theme.mjs.map +1 -0
- package/package.json +190 -4
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3421 @@
|
|
|
1
|
+
// src/cookies.ts
|
|
2
|
+
var PART = 3500;
|
|
3
|
+
function readCookies(header) {
|
|
4
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const pair of (header ?? "").split(";")) {
|
|
6
|
+
const i = pair.indexOf("=");
|
|
7
|
+
if (i > 0) out2.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim());
|
|
8
|
+
}
|
|
9
|
+
return out2;
|
|
10
|
+
}
|
|
11
|
+
function readParts(cookies, name) {
|
|
12
|
+
let value = "";
|
|
13
|
+
for (let i = 0; cookies.has(`${name}.${i}`); i++) value += cookies.get(`${name}.${i}`);
|
|
14
|
+
return value === "" ? null : value;
|
|
15
|
+
}
|
|
16
|
+
function attributes(a) {
|
|
17
|
+
return `Path=${a.path ?? "/"}; HttpOnly; SameSite=Lax${a.maxAge === void 0 ? "" : `; Max-Age=${a.maxAge}`}${a.secure ? "; Secure" : ""}`;
|
|
18
|
+
}
|
|
19
|
+
function writeCookie(name, value, a) {
|
|
20
|
+
return `${name}=${value}; ${attributes(a)}`;
|
|
21
|
+
}
|
|
22
|
+
function writeParts(name, value, a, had) {
|
|
23
|
+
const out2 = [];
|
|
24
|
+
let n = 0;
|
|
25
|
+
for (let i = 0; i < value.length; i += PART) out2.push(writeCookie(`${name}.${n++}`, value.slice(i, i + PART), a));
|
|
26
|
+
for (let i = n; had.has(`${name}.${i}`); i++) out2.push(writeCookie(`${name}.${i}`, "", { ...a, maxAge: 0 }));
|
|
27
|
+
return out2;
|
|
28
|
+
}
|
|
29
|
+
function clearParts(name, a, had) {
|
|
30
|
+
const out2 = [];
|
|
31
|
+
for (let i = 0; had.has(`${name}.${i}`) || i === 0; i++) out2.push(writeCookie(`${name}.${i}`, "", { ...a, maxAge: 0 }));
|
|
32
|
+
return out2;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/seal.ts
|
|
36
|
+
import { CompactEncrypt, compactDecrypt } from "jose";
|
|
37
|
+
var encoder = new TextEncoder();
|
|
38
|
+
var decoder = new TextDecoder();
|
|
39
|
+
async function keyOf(secret) {
|
|
40
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(secret)));
|
|
41
|
+
}
|
|
42
|
+
async function seal(value, key) {
|
|
43
|
+
return new CompactEncrypt(encoder.encode(JSON.stringify(value))).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).encrypt(key);
|
|
44
|
+
}
|
|
45
|
+
async function unseal(sealed, key) {
|
|
46
|
+
try {
|
|
47
|
+
const { plaintext } = await compactDecrypt(sealed, key, {
|
|
48
|
+
keyManagementAlgorithms: ["dir"],
|
|
49
|
+
contentEncryptionAlgorithms: ["A256GCM"]
|
|
50
|
+
});
|
|
51
|
+
return JSON.parse(decoder.decode(plaintext));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function unsealAny(sealed, ring) {
|
|
57
|
+
for (const key of ring) {
|
|
58
|
+
const value = await unseal(sealed, key);
|
|
59
|
+
if (value !== null) return value;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/types.ts
|
|
65
|
+
var CauthyConfigError = class extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(`@cauthyhq/node: ${message}`);
|
|
68
|
+
this.name = "CauthyConfigError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/url.ts
|
|
73
|
+
function withoutTrailingSlashes(s) {
|
|
74
|
+
let end = s.length;
|
|
75
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
|
76
|
+
return s.slice(0, end);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/contract.ts
|
|
80
|
+
var STANDARD_VARIABLES = [
|
|
81
|
+
"CAUTHY_ISSUER",
|
|
82
|
+
"CAUTHY_CLIENT_ID",
|
|
83
|
+
"CAUTHY_CLIENT_SECRET",
|
|
84
|
+
"CAUTHY_SECRET",
|
|
85
|
+
"CAUTHY_SYSTEM",
|
|
86
|
+
"CAUTHY_TENANT",
|
|
87
|
+
"CAUTHY_SYSTEM_KEY",
|
|
88
|
+
"CAUTHY_SYSTEM_KEY_ID"
|
|
89
|
+
];
|
|
90
|
+
function failClosed(environment) {
|
|
91
|
+
const set = STANDARD_VARIABLES.filter((name) => (environment?.[name] ?? "") !== "");
|
|
92
|
+
if (set.length > 0) {
|
|
93
|
+
throw new CauthyConfigError(`the application runs by contract, and ${set.join(", ")} ${set.length === 1 ? "is" : "are"} also set: remove ` + (set.length === 1 ? "it" : "them") + " from the environment. Everything comes from the contract.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
var SLUG = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
|
|
97
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
98
|
+
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
99
|
+
var text = (v) => typeof v === "string" ? v : "";
|
|
100
|
+
function httpsAddress(raw, what) {
|
|
101
|
+
let u;
|
|
102
|
+
try {
|
|
103
|
+
u = new URL(raw);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new CauthyConfigError(`the contract's ${what} is not an address: ${JSON.stringify(raw)}`);
|
|
106
|
+
}
|
|
107
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
108
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
109
|
+
throw new CauthyConfigError(`the contract's ${what} must be https: ${raw}`);
|
|
110
|
+
}
|
|
111
|
+
return u;
|
|
112
|
+
}
|
|
113
|
+
function readContract(options) {
|
|
114
|
+
const { system } = options;
|
|
115
|
+
const provider = options.provider ?? "cauthy";
|
|
116
|
+
if (!SLUG.test(system ?? "")) throw new CauthyConfigError(`contract.system must be the system's id in the graph, e.g. "acme-crm"`);
|
|
117
|
+
const graph = options.graph && typeof options.graph === "object" && "default" in options.graph && !("edges" in options.graph) ? options.graph.default : options.graph;
|
|
118
|
+
if (!graph || !Array.isArray(graph.edges) || !Array.isArray(graph.nodes)) {
|
|
119
|
+
throw new CauthyConfigError("contract.graph is not a context graph: pass the graph.json that `cauthy contracts pull` writes");
|
|
120
|
+
}
|
|
121
|
+
const node = (id) => graph.nodes?.find((n) => n.type === "system" && n.id === id);
|
|
122
|
+
const own = node(system);
|
|
123
|
+
if (!own) throw new CauthyConfigError(`the graph has no system "${system}"`);
|
|
124
|
+
const cauthy = node(provider);
|
|
125
|
+
if (!cauthy) throw new CauthyConfigError(`the graph has no system "${provider}"`);
|
|
126
|
+
const links = graph.edges.filter((e2) => e2.relation === "links" && e2.from === system && e2.to === provider && e2.sign_in).filter((e2) => options.link === void 0 || e2.link === options.link);
|
|
127
|
+
if (links.length === 0) {
|
|
128
|
+
throw new CauthyConfigError(options.link ? `the graph has no link "${options.link}" from ${system} to ${provider} with sign_in` : `no link of the graph signs ${system}'s people in with ${provider}: merge one in the contracts repository, then pull the graph again`);
|
|
129
|
+
}
|
|
130
|
+
if (links.length > 1) {
|
|
131
|
+
throw new CauthyConfigError(`${system} has ${links.length} links to ${provider}: name the one to read (contract.link): ${links.map((l) => text(l.link)).join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
const e = links[0];
|
|
134
|
+
const link = text(e.link);
|
|
135
|
+
const named = text(e.identity?.tenant);
|
|
136
|
+
const pinned = text(e.identity?.tenant_id);
|
|
137
|
+
if (named === NIL_UUID || pinned === NIL_UUID) throw new CauthyConfigError(`the link ${link} names the nil UUID as its workspace: name the workspace by its slug`);
|
|
138
|
+
let tenantId;
|
|
139
|
+
let tenant;
|
|
140
|
+
if (UUID.test(named)) {
|
|
141
|
+
if (pinned !== "" && pinned.toLowerCase() !== named.toLowerCase()) {
|
|
142
|
+
throw new CauthyConfigError(`the link ${link} names the workspace ${named} and its tenant_id ${pinned} does not match: pull the graph again`);
|
|
143
|
+
}
|
|
144
|
+
tenantId = named;
|
|
145
|
+
tenant = text((cauthy.tenants ?? []).find((t) => t.id === named)?.slug);
|
|
146
|
+
if (tenant === "") throw new CauthyConfigError(`the graph does not name the slug of the workspace ${named} that the link ${link} uses`);
|
|
147
|
+
} else {
|
|
148
|
+
if (!SLUG.test(named)) throw new CauthyConfigError(`the link ${link} names no workspace: identity.tenant is a UUID or a slug`);
|
|
149
|
+
if (!UUID.test(pinned)) {
|
|
150
|
+
throw new CauthyConfigError(e.identity?.create_tenant === true ? `the link ${link} asks Cauthy for a new workspace "${named}", and the graph has no tenant_id for it yet: once Cauthy has created it and core-contracts lists it, pull the graph again` : `the link ${link} names the workspace "${named}" by its slug, and the graph carries no tenant_id for it: pull the graph again`);
|
|
151
|
+
}
|
|
152
|
+
tenantId = pinned;
|
|
153
|
+
tenant = named;
|
|
154
|
+
}
|
|
155
|
+
const issuer = withoutTrailingSlashes(httpsAddress(text(cauthy.url), `address of ${provider}`).href);
|
|
156
|
+
const signIn = e.sign_in ?? {};
|
|
157
|
+
const clientId = text(signIn.client_id) || `pact.${link}`;
|
|
158
|
+
const redirectUris = Array.isArray(signIn.redirect_uris) ? signIn.redirect_uris.map(text).filter(Boolean) : [];
|
|
159
|
+
if (redirectUris.length === 0) throw new CauthyConfigError(`the link ${link} names no redirect URI`);
|
|
160
|
+
const proof = text(signIn.system_proof);
|
|
161
|
+
if (signIn.system_proof !== void 0 && proof !== "off" && proof !== "auto" && proof !== "required") {
|
|
162
|
+
throw new CauthyConfigError(`the link ${link} has system_proof ${JSON.stringify(signIn.system_proof)}: it is off, auto or required. Update @cauthyhq/node, or pull the graph again`);
|
|
163
|
+
}
|
|
164
|
+
const systemProof = proof === "off" || proof === "auto" || proof === "required" ? proof : signIn.require_system_proof === true ? "required" : "off";
|
|
165
|
+
if (systemProof !== "off" && !e.service) {
|
|
166
|
+
throw new CauthyConfigError(`the link ${link} asks for the system's proof (${systemProof}) and gives ${system} no identity to make it with: it needs a service block`);
|
|
167
|
+
}
|
|
168
|
+
const out2 = {
|
|
169
|
+
system,
|
|
170
|
+
link,
|
|
171
|
+
issuer,
|
|
172
|
+
tenantId,
|
|
173
|
+
tenant,
|
|
174
|
+
clientId,
|
|
175
|
+
redirectUris,
|
|
176
|
+
requireSystemProof: systemProof === "required",
|
|
177
|
+
systemProof,
|
|
178
|
+
requireVerifiedEmail: signIn.require_verified_email === true,
|
|
179
|
+
persistentSessions: signIn.persistent_sessions === true
|
|
180
|
+
};
|
|
181
|
+
if (e.service) {
|
|
182
|
+
out2.serviceClientId = text(e.service.client_id) || `pact.${link}.service`;
|
|
183
|
+
const jwks = text(e.service.jwks_url) || text(own.jwks_url);
|
|
184
|
+
if (jwks === "") throw new CauthyConfigError(`the link ${link} gives ${system} its own identity, and ${system} names no jwks_url`);
|
|
185
|
+
out2.jwksUrl = httpsAddress(jwks, "jwks_url").href;
|
|
186
|
+
out2.jwksPath = new URL(out2.jwksUrl).pathname;
|
|
187
|
+
}
|
|
188
|
+
if (text(own.events_url) !== "") out2.eventsUrl = httpsAddress(text(own.events_url), "events_url").href;
|
|
189
|
+
return out2;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/keys.ts
|
|
193
|
+
import { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK } from "jose";
|
|
194
|
+
var RETIRE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
195
|
+
var FORGET_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
196
|
+
var CURRENT_TTL_MS = 5 * 60 * 1e3;
|
|
197
|
+
var JWKS_MAX_AGE_S = 300;
|
|
198
|
+
var SIGN_AFTER_MS = (JWKS_MAX_AGE_S + 60) * 1e3;
|
|
199
|
+
var SETTLING_TTL_MS = 5 * 1e3;
|
|
200
|
+
var newestFirst = (keys2) => [...keys2].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.kid.localeCompare(a.kid));
|
|
201
|
+
function published(key) {
|
|
202
|
+
const { kty, n, e } = key.publicJwk;
|
|
203
|
+
return { kty, n, e, kid: key.kid, alg: "RS256", use: "sig" };
|
|
204
|
+
}
|
|
205
|
+
function pactKeys(store, sealKeys, now = Date.now) {
|
|
206
|
+
let remembered;
|
|
207
|
+
let making;
|
|
208
|
+
async function make(after) {
|
|
209
|
+
const { publicKey, privateKey } = await generateKeyPair("RS256", { modulusLength: 2048, extractable: true });
|
|
210
|
+
const publicJwk = await exportJWK(publicKey);
|
|
211
|
+
const kid = await calculateJwkThumbprint(publicJwk);
|
|
212
|
+
const at = Math.max(now(), after ? Date.parse(after.createdAt) + 1 : 0);
|
|
213
|
+
const key = { kid, publicJwk, sealed: await seal(await exportJWK(privateKey), (await sealKeys)[0]), createdAt: new Date(at).toISOString() };
|
|
214
|
+
if (!after && store.addFirst) await store.addFirst(key);
|
|
215
|
+
else await store.add(key);
|
|
216
|
+
return key;
|
|
217
|
+
}
|
|
218
|
+
const age = (key) => now() - Date.parse(key.createdAt);
|
|
219
|
+
function signer(kept) {
|
|
220
|
+
const ripe = kept.find((k) => age(k) >= SIGN_AFTER_MS);
|
|
221
|
+
if (!ripe) return { key: kept[kept.length - 1], until: now() + SETTLING_TTL_MS };
|
|
222
|
+
const until = ripe === kept[0] ? now() + CURRENT_TTL_MS : Math.min(now() + CURRENT_TTL_MS, Date.parse(kept[0].createdAt) + SIGN_AFTER_MS);
|
|
223
|
+
return { key: ripe, until };
|
|
224
|
+
}
|
|
225
|
+
async function open(key) {
|
|
226
|
+
const ring = await sealKeys;
|
|
227
|
+
for (let i = 0; i < ring.length; i++) {
|
|
228
|
+
const jwk = await unseal(key.sealed, ring[i]);
|
|
229
|
+
if (jwk !== null) return { jwk, fresh: i === 0 };
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
async function keptKeys() {
|
|
234
|
+
const kept = newestFirst(await store.list());
|
|
235
|
+
if (kept.length > 0) return kept;
|
|
236
|
+
making ??= make().finally(() => {
|
|
237
|
+
making = void 0;
|
|
238
|
+
});
|
|
239
|
+
await making;
|
|
240
|
+
const again = newestFirst(await store.list());
|
|
241
|
+
if (again.length === 0) throw new CauthyConfigError("the key store kept no key: check that it writes");
|
|
242
|
+
return again;
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
async current() {
|
|
246
|
+
if (remembered && remembered.until > now()) return remembered;
|
|
247
|
+
const { key, until } = signer(await keptKeys());
|
|
248
|
+
if (remembered?.kid === key.kid) {
|
|
249
|
+
remembered.until = until;
|
|
250
|
+
return remembered;
|
|
251
|
+
}
|
|
252
|
+
const opened = await open(key);
|
|
253
|
+
if (opened === null) {
|
|
254
|
+
throw new CauthyConfigError(`the key ${key.kid} cannot be opened with the root key (PACT_ROOT_KEY): if the root key was rotated, set the previous one as PACT_ROOT_KEY_PREVIOUS`);
|
|
255
|
+
}
|
|
256
|
+
remembered = { kid: key.kid, privateKey: await importJWK(opened.jwk, "RS256"), until };
|
|
257
|
+
return remembered;
|
|
258
|
+
},
|
|
259
|
+
async published() {
|
|
260
|
+
const kept = await keptKeys();
|
|
261
|
+
const out2 = [];
|
|
262
|
+
for (let i = 0; i < kept.length; i++) {
|
|
263
|
+
if (i === 0 || now() - Date.parse(kept[i - 1].createdAt) < RETIRE_AFTER_MS) out2.push(published(kept[i]));
|
|
264
|
+
else if (now() - Date.parse(kept[i - 1].createdAt) > FORGET_AFTER_MS) await store.remove(kept[i].kid);
|
|
265
|
+
}
|
|
266
|
+
return { keys: out2 };
|
|
267
|
+
},
|
|
268
|
+
async rotate() {
|
|
269
|
+
const key = await make(newestFirst(await store.list())[0]);
|
|
270
|
+
remembered = void 0;
|
|
271
|
+
return key.kid;
|
|
272
|
+
},
|
|
273
|
+
async reseal() {
|
|
274
|
+
let changed = 0;
|
|
275
|
+
const ring = await sealKeys;
|
|
276
|
+
for (const key of await store.list()) {
|
|
277
|
+
const opened = await open(key);
|
|
278
|
+
if (opened === null || opened.fresh) continue;
|
|
279
|
+
await store.update({ ...key, sealed: await seal(opened.jwk, ring[0]) });
|
|
280
|
+
changed++;
|
|
281
|
+
}
|
|
282
|
+
return changed;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/rootkey.ts
|
|
288
|
+
var encoder2 = new TextEncoder();
|
|
289
|
+
var MIN_ROOT_KEY = 32;
|
|
290
|
+
async function derive(root, purpose) {
|
|
291
|
+
const ikm = await crypto.subtle.importKey("raw", encoder2.encode(root), "HKDF", false, ["deriveBits"]);
|
|
292
|
+
const bits = await crypto.subtle.deriveBits(
|
|
293
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: encoder2.encode(`pact/${purpose}/v1`) },
|
|
294
|
+
ikm,
|
|
295
|
+
256
|
|
296
|
+
);
|
|
297
|
+
return new Uint8Array(bits);
|
|
298
|
+
}
|
|
299
|
+
function deriveKeys(root, previous) {
|
|
300
|
+
if (root.length < MIN_ROOT_KEY) {
|
|
301
|
+
throw new CauthyConfigError(`rootKey (PACT_ROOT_KEY) must be at least ${MIN_ROOT_KEY} characters: 32 random bytes, e.g. openssl rand -base64 32`);
|
|
302
|
+
}
|
|
303
|
+
if (previous !== void 0 && previous !== "" && previous.length < MIN_ROOT_KEY) {
|
|
304
|
+
throw new CauthyConfigError(`the previous root key (PACT_ROOT_KEY_PREVIOUS) must be at least ${MIN_ROOT_KEY} characters`);
|
|
305
|
+
}
|
|
306
|
+
const roots = previous && previous !== root ? [root, previous] : [root];
|
|
307
|
+
const ring = (purpose) => Promise.all(roots.map((r) => derive(r, purpose)));
|
|
308
|
+
return { seal: ring("seal"), cookie: ring("cookie"), flow: ring("flow") };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/deadline.ts
|
|
312
|
+
var DEFAULT_DEADLINE_MS = 5e3;
|
|
313
|
+
var isTimeout = (err) => err?.name === "TimeoutError";
|
|
314
|
+
function deadline(base, ms) {
|
|
315
|
+
return async (input, init = {}) => {
|
|
316
|
+
const send = base ?? fetch;
|
|
317
|
+
const stream = new Headers(init.headers).get("accept")?.includes("text/event-stream") === true;
|
|
318
|
+
if (!stream) {
|
|
319
|
+
const limit = AbortSignal.timeout(ms);
|
|
320
|
+
return send(input, { ...init, signal: init.signal ? anySignal([init.signal, limit]) : limit });
|
|
321
|
+
}
|
|
322
|
+
const own = new AbortController();
|
|
323
|
+
const timer = setTimeout(() => own.abort(new DOMException(`Cauthy did not answer within ${ms} ms`, "TimeoutError")), ms);
|
|
324
|
+
try {
|
|
325
|
+
return await send(input, { ...init, signal: init.signal ? anySignal([init.signal, own.signal]) : own.signal });
|
|
326
|
+
} finally {
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function anySignal(signals) {
|
|
332
|
+
if (typeof AbortSignal.any === "function") return AbortSignal.any(signals);
|
|
333
|
+
const controller = new AbortController();
|
|
334
|
+
for (const signal of signals) {
|
|
335
|
+
if (signal.aborted) {
|
|
336
|
+
controller.abort(signal.reason);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
340
|
+
}
|
|
341
|
+
return controller.signal;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/permissions.ts
|
|
345
|
+
function matches(entry, resource, action) {
|
|
346
|
+
const i = entry.lastIndexOf(":");
|
|
347
|
+
if (i < 0) return false;
|
|
348
|
+
const r = entry.slice(0, i);
|
|
349
|
+
const a = entry.slice(i + 1);
|
|
350
|
+
return (r === "*" || r === resource) && (a === "*" || a === action);
|
|
351
|
+
}
|
|
352
|
+
function can(organization, resource, action) {
|
|
353
|
+
if (!organization?.permissions) return false;
|
|
354
|
+
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
355
|
+
return organization.permissions.some((p) => matches(p, resource, action));
|
|
356
|
+
}
|
|
357
|
+
function definePolicy(given) {
|
|
358
|
+
const roles = Array.isArray(given) ? given : given.roles;
|
|
359
|
+
const resources = Array.isArray(given) ? [] : given.catalog ?? [];
|
|
360
|
+
const entry = (p) => `${p.resource}:${p.action}`;
|
|
361
|
+
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
362
|
+
const split = (permission) => {
|
|
363
|
+
const i = permission.lastIndexOf(":");
|
|
364
|
+
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
roles: roles.map((r) => ({
|
|
368
|
+
role: r.role,
|
|
369
|
+
...r.name !== void 0 ? { name: r.name } : {},
|
|
370
|
+
...r.description !== void 0 ? { description: r.description } : {},
|
|
371
|
+
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
372
|
+
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
373
|
+
})),
|
|
374
|
+
catalog: resources.map((c) => ({
|
|
375
|
+
resource: c.resource,
|
|
376
|
+
...c.description !== void 0 ? { description: c.description } : {},
|
|
377
|
+
actions: [...c.actions],
|
|
378
|
+
...c.attributes ? { attributes: { ...c.attributes } } : {}
|
|
379
|
+
})),
|
|
380
|
+
roleNames: roles.map((r) => r.role),
|
|
381
|
+
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
382
|
+
can(organization, permission) {
|
|
383
|
+
const [resource, action] = split(permission);
|
|
384
|
+
return can(organization, resource, action);
|
|
385
|
+
},
|
|
386
|
+
hasRole(organization, ...wanted) {
|
|
387
|
+
return organization != null && wanted.includes(organization.role);
|
|
388
|
+
},
|
|
389
|
+
permissionsOf(role) {
|
|
390
|
+
const seen = /* @__PURE__ */ new Set();
|
|
391
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
392
|
+
const walk = (name) => {
|
|
393
|
+
if (seen.has(name)) return;
|
|
394
|
+
seen.add(name);
|
|
395
|
+
const r = byName.get(name);
|
|
396
|
+
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
397
|
+
for (const parent of r?.inherits ?? []) walk(parent);
|
|
398
|
+
};
|
|
399
|
+
walk(role);
|
|
400
|
+
return [...out2];
|
|
401
|
+
},
|
|
402
|
+
// The types did the work; this only hands the question on, as a method of
|
|
403
|
+
// the asker so it keeps its `this`.
|
|
404
|
+
check(asker, ...args) {
|
|
405
|
+
return asker.check(...args);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
410
|
+
function policySource(system, read) {
|
|
411
|
+
const roles = Array.isArray(read) ? read : read.roles;
|
|
412
|
+
const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
|
|
413
|
+
const text2 = (v) => JSON.stringify(v);
|
|
414
|
+
const key = (k) => IDENT.test(k) ? k : text2(k);
|
|
415
|
+
const one = (r, pad) => {
|
|
416
|
+
const parts = [`role: ${text2(r.role)}`];
|
|
417
|
+
if (r.name) parts.push(`name: ${text2(r.name)}`);
|
|
418
|
+
if (r.description) parts.push(`description: ${text2(r.description)}`);
|
|
419
|
+
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text2).join(", ")}]`);
|
|
420
|
+
if (r.permissions?.length) {
|
|
421
|
+
const list = r.permissions.map((p) => `${pad} { ${[
|
|
422
|
+
...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text2(p.kind)}`] : [],
|
|
423
|
+
`resource: ${text2(p.resource)}`,
|
|
424
|
+
`action: ${text2(p.action)}`,
|
|
425
|
+
...p.condition ? [`condition: ${text2(p.condition)}`] : []
|
|
426
|
+
].join(", ")} },`);
|
|
427
|
+
parts.push(`permissions: [
|
|
428
|
+
${list.join("\n")}
|
|
429
|
+
${pad} ]`);
|
|
430
|
+
}
|
|
431
|
+
return `${pad} {
|
|
432
|
+
${pad} ${parts.join(`,
|
|
433
|
+
${pad} `)},
|
|
434
|
+
${pad} },`;
|
|
435
|
+
};
|
|
436
|
+
const resource = (c) => {
|
|
437
|
+
const parts = [`resource: ${text2(c.resource)}`];
|
|
438
|
+
if (c.description) parts.push(`description: ${text2(c.description)}`);
|
|
439
|
+
parts.push(`actions: [${c.actions.map(text2).join(", ")}]`);
|
|
440
|
+
const attrs = Object.entries(c.attributes ?? {});
|
|
441
|
+
if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${text2(v)}`).join(", ")} }`);
|
|
442
|
+
return ` { ${parts.join(", ")} },`;
|
|
443
|
+
};
|
|
444
|
+
const head = `// The catalog of roles of ${text2(system)}, as Cauthy has it.
|
|
445
|
+
// Written by \`pullPolicy\`: run it again after the catalog
|
|
446
|
+
// changes, and do not edit this file by hand.
|
|
447
|
+
`;
|
|
448
|
+
if (catalog.length === 0) {
|
|
449
|
+
return `${head}
|
|
450
|
+
import { definePolicy, type PermissionOf, type RoleOf } from '@cauthyhq/node'
|
|
451
|
+
|
|
452
|
+
export const policy = definePolicy([
|
|
453
|
+
${roles.map((r) => one(r, "")).join("\n")}
|
|
454
|
+
])
|
|
455
|
+
|
|
456
|
+
export const { can, hasRole } = policy
|
|
457
|
+
export type Role = RoleOf<typeof policy>
|
|
458
|
+
export type Permission = PermissionOf<typeof policy>
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
return `${head}
|
|
462
|
+
import { definePolicy, type CheckOf, type PermissionOf, type RoleOf } from '@cauthyhq/node'
|
|
463
|
+
|
|
464
|
+
export const policy = definePolicy({
|
|
465
|
+
catalog: [
|
|
466
|
+
${catalog.map(resource).join("\n")}
|
|
467
|
+
],
|
|
468
|
+
roles: [
|
|
469
|
+
${roles.map((r) => one(r, " ")).join("\n")}
|
|
470
|
+
],
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
export const { can, hasRole } = policy
|
|
474
|
+
export type Role = RoleOf<typeof policy>
|
|
475
|
+
export type Permission = PermissionOf<typeof policy>
|
|
476
|
+
/** What a check may ask: \`policy.check(auth, request, { organization, ...question })\`. */
|
|
477
|
+
export type Check = CheckOf<typeof policy>
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
function reach(o, doing) {
|
|
481
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
482
|
+
const r = {
|
|
483
|
+
issuer: withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? ""),
|
|
484
|
+
system: o.system ?? env4("CAUTHY_SYSTEM") ?? "",
|
|
485
|
+
tenant: o.tenant ?? env4("CAUTHY_TENANT") ?? "",
|
|
486
|
+
token: o.token ?? env4("CAUTHY_TOKEN") ?? ""
|
|
487
|
+
};
|
|
488
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: r.issuer, CAUTHY_SYSTEM: r.system, CAUTHY_TENANT: r.tenant, CAUTHY_TOKEN: r.token })) {
|
|
489
|
+
if (v === "") throw new Error(`@cauthyhq/node: ${name} is required to ${doing}`);
|
|
490
|
+
}
|
|
491
|
+
return { ...r, fetch: deadline(o.fetch, o.timeoutMs ?? DEFAULT_DEADLINE_MS) };
|
|
492
|
+
}
|
|
493
|
+
async function systemCall(r, method, what, body) {
|
|
494
|
+
const res = await r.fetch(`${r.issuer}/v1/systems/${encodeURIComponent(r.system)}/${what}`, {
|
|
495
|
+
...method === "PUT" ? { method } : {},
|
|
496
|
+
headers: {
|
|
497
|
+
authorization: `Bearer ${r.token}`,
|
|
498
|
+
"x-tenant-key": r.tenant,
|
|
499
|
+
...body === void 0 ? { accept: "application/json" } : { "content-type": "application/json" }
|
|
500
|
+
},
|
|
501
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
502
|
+
});
|
|
503
|
+
const answer = await res.json().catch(() => null);
|
|
504
|
+
if (!res.ok) {
|
|
505
|
+
const verb = method === "GET" ? `refused to give the ${what}` : `refused the ${what}`;
|
|
506
|
+
throw new Error(`@cauthyhq/node: Cauthy ${verb} (${res.status}): ${answer?.errors?.[0]?.message ?? res.statusText}`);
|
|
507
|
+
}
|
|
508
|
+
return answer?.data;
|
|
509
|
+
}
|
|
510
|
+
async function pullPolicy(o = {}) {
|
|
511
|
+
const data = await systemCall(reach(o, "read a policy"), "GET", "policy");
|
|
512
|
+
return data?.roles ?? [];
|
|
513
|
+
}
|
|
514
|
+
async function pullCatalog(o = {}) {
|
|
515
|
+
const data = await systemCall(reach(o, "read a catalog"), "GET", "catalog");
|
|
516
|
+
return data?.resources ?? [];
|
|
517
|
+
}
|
|
518
|
+
async function pushPolicy(o) {
|
|
519
|
+
const r = reach(o, "push a policy");
|
|
520
|
+
if (o.catalog !== void 0) await systemCall(r, "PUT", "catalog", { resources: o.catalog });
|
|
521
|
+
await systemCall(r, "PUT", "policy", { roles: o.roles });
|
|
522
|
+
}
|
|
523
|
+
async function pushCatalog(o) {
|
|
524
|
+
const data = await systemCall(reach(o, "push a catalog"), "PUT", "catalog", { resources: o.resources });
|
|
525
|
+
return data?.resources ?? [];
|
|
526
|
+
}
|
|
527
|
+
function checkWire(q) {
|
|
528
|
+
return {
|
|
529
|
+
resource: q.resource,
|
|
530
|
+
action: q.action,
|
|
531
|
+
...q.resourceId !== void 0 ? { resource_id: q.resourceId } : {},
|
|
532
|
+
...q.ownerId !== void 0 ? { owner_id: q.ownerId } : {},
|
|
533
|
+
...q.attributes !== void 0 ? { attributes: q.attributes } : {}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function checkResultOf(d) {
|
|
537
|
+
const r = d?.rule;
|
|
538
|
+
const rule = r && typeof r === "object" ? {
|
|
539
|
+
source: r.source === "exception" ? "exception" : "role",
|
|
540
|
+
...r.role ? { role: String(r.role) } : {},
|
|
541
|
+
kind: String(r.kind ?? "*"),
|
|
542
|
+
resource: String(r.resource ?? ""),
|
|
543
|
+
action: String(r.action ?? ""),
|
|
544
|
+
effect: r.effect === "allow" ? "allow" : "deny",
|
|
545
|
+
...r.condition ? { condition: String(r.condition) } : {}
|
|
546
|
+
} : void 0;
|
|
547
|
+
return { allowed: d?.allowed === true, reason: String(d?.reason ?? ""), ...rule ? { rule } : {} };
|
|
548
|
+
}
|
|
549
|
+
function organizationIdOf(o) {
|
|
550
|
+
return typeof o === "string" ? o : typeof o?.id === "string" ? o.id : "";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/system.ts
|
|
554
|
+
import { exportJWK as exportJWK2, importJWK as importJWK2, importPKCS8, SignJWT } from "jose";
|
|
555
|
+
|
|
556
|
+
// src/theme.ts
|
|
557
|
+
var MAX_RADIUS = 40;
|
|
558
|
+
function withoutComments(css) {
|
|
559
|
+
let out2 = "";
|
|
560
|
+
let i = 0;
|
|
561
|
+
for (; ; ) {
|
|
562
|
+
const open = css.indexOf("/*", i);
|
|
563
|
+
if (open < 0) return out2 + css.slice(i);
|
|
564
|
+
out2 += css.slice(i, open);
|
|
565
|
+
const close = css.indexOf("*/", open + 2);
|
|
566
|
+
if (close < 0) return out2;
|
|
567
|
+
i = close + 2;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function declarations(css) {
|
|
571
|
+
const text2 = withoutComments(css);
|
|
572
|
+
const out2 = [];
|
|
573
|
+
const within = [];
|
|
574
|
+
let chunk = "";
|
|
575
|
+
let quote = "";
|
|
576
|
+
let depth = 0;
|
|
577
|
+
const flush = () => {
|
|
578
|
+
const m = /^\s*(--[\w-]+)\s*:\s*([\s\S]+?)\s*$/.exec(chunk);
|
|
579
|
+
if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
|
|
580
|
+
chunk = "";
|
|
581
|
+
};
|
|
582
|
+
for (const ch of text2) {
|
|
583
|
+
if (quote) {
|
|
584
|
+
chunk += ch;
|
|
585
|
+
if (ch === quote) quote = "";
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (ch === '"' || ch === "'") {
|
|
589
|
+
quote = ch;
|
|
590
|
+
chunk += ch;
|
|
591
|
+
} else if (ch === "(") {
|
|
592
|
+
depth++;
|
|
593
|
+
chunk += ch;
|
|
594
|
+
} else if (ch === ")") {
|
|
595
|
+
depth = Math.max(0, depth - 1);
|
|
596
|
+
chunk += ch;
|
|
597
|
+
} else if (ch === "{" && depth === 0) {
|
|
598
|
+
within.push(chunk.trim().replace(/\s+/g, " "));
|
|
599
|
+
chunk = "";
|
|
600
|
+
} else if (ch === "}" && depth === 0) {
|
|
601
|
+
flush();
|
|
602
|
+
within.pop();
|
|
603
|
+
} else if (ch === ";" && depth === 0) {
|
|
604
|
+
flush();
|
|
605
|
+
} else {
|
|
606
|
+
chunk += ch;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return out2;
|
|
610
|
+
}
|
|
611
|
+
var DARK = /prefers-color-scheme:\s*dark|data-(mode|theme|color-scheme)=["']?dark|\.dark\b/i;
|
|
612
|
+
var LIGHT = /prefers-color-scheme:\s*light|data-(mode|theme|color-scheme)=["']?light|\.light\b/i;
|
|
613
|
+
var NAMED = /data-theme=["']?([\w-]+)/i;
|
|
614
|
+
function applies(d, o) {
|
|
615
|
+
for (const scope of d.within) {
|
|
616
|
+
if (o.mode === "light" && DARK.test(scope)) return false;
|
|
617
|
+
if (o.mode === "dark" && LIGHT.test(scope)) return false;
|
|
618
|
+
const named = NAMED.exec(scope);
|
|
619
|
+
if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
|
|
620
|
+
if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host|^\.(dark|light)\b/.test(scope)) return false;
|
|
621
|
+
}
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
var TOKEN_CHARS = /^[A-Za-z0-9 #%.,()/'"_+*-]+$/;
|
|
625
|
+
var TOKEN_FUNCS = /* @__PURE__ */ new Set([
|
|
626
|
+
"var",
|
|
627
|
+
"calc",
|
|
628
|
+
"min",
|
|
629
|
+
"max",
|
|
630
|
+
"clamp",
|
|
631
|
+
"rgb",
|
|
632
|
+
"rgba",
|
|
633
|
+
"hsl",
|
|
634
|
+
"hsla",
|
|
635
|
+
"hwb",
|
|
636
|
+
"lab",
|
|
637
|
+
"lch",
|
|
638
|
+
"oklab",
|
|
639
|
+
"oklch",
|
|
640
|
+
"color",
|
|
641
|
+
"color-mix",
|
|
642
|
+
"light-dark",
|
|
643
|
+
"cubic-bezier",
|
|
644
|
+
"steps",
|
|
645
|
+
"linear",
|
|
646
|
+
"linear-gradient",
|
|
647
|
+
"radial-gradient",
|
|
648
|
+
"conic-gradient",
|
|
649
|
+
"repeating-linear-gradient",
|
|
650
|
+
"repeating-radial-gradient",
|
|
651
|
+
"drop-shadow",
|
|
652
|
+
"blur",
|
|
653
|
+
"translate",
|
|
654
|
+
"translatex",
|
|
655
|
+
"translatey",
|
|
656
|
+
"scale",
|
|
657
|
+
"rotate"
|
|
658
|
+
]);
|
|
659
|
+
function tokenValue(value) {
|
|
660
|
+
const v = value.replace(/\s+/g, " ").trim();
|
|
661
|
+
if (v === "" || v.length > 400 || !TOKEN_CHARS.test(v)) return null;
|
|
662
|
+
for (const m of v.matchAll(/([A-Za-z-]+)\(/g)) if (!TOKEN_FUNCS.has(m[1].toLowerCase())) return null;
|
|
663
|
+
if (/^\(|[ ,(]\(/.test(v)) return null;
|
|
664
|
+
return v;
|
|
665
|
+
}
|
|
666
|
+
var THEME_SELECTOR = /^(?::root|html|body|\[data-(?:theme|mode|color-scheme)(?:=["']?[\w-]+["']?)?\])$/i;
|
|
667
|
+
var isThemeScope = (selectors) => {
|
|
668
|
+
const parts = selectors.split(/[\s,]+/).filter(Boolean);
|
|
669
|
+
return parts.length > 0 && parts.every((one) => THEME_SELECTOR.test(one));
|
|
670
|
+
};
|
|
671
|
+
function tokensFromStackmyth(css, options = {}) {
|
|
672
|
+
const mode = options.mode ?? "light";
|
|
673
|
+
const all = (Array.isArray(css) ? css : [css]).flatMap(declarations);
|
|
674
|
+
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);
|
|
675
|
+
const vars = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const d of all) {
|
|
677
|
+
if (!applies(d, { mode, theme: named })) continue;
|
|
678
|
+
if (!d.within.every((w) => w.startsWith("@") || isThemeScope(w))) continue;
|
|
679
|
+
vars.set(d.name, d.value);
|
|
680
|
+
}
|
|
681
|
+
const inline = (value, depth = 0) => {
|
|
682
|
+
if (depth > 16) return null;
|
|
683
|
+
let failed = false;
|
|
684
|
+
let out2 = value;
|
|
685
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
686
|
+
const next = out2.replace(/var\(\s*(--(?!sm-)[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
|
|
687
|
+
const known = vars.get(name);
|
|
688
|
+
const got = known !== void 0 ? inline(known, depth + 1) : fallback !== void 0 ? inline(fallback.trim(), depth + 1) : null;
|
|
689
|
+
if (got === null) failed = true;
|
|
690
|
+
return got ?? "";
|
|
691
|
+
});
|
|
692
|
+
if (failed) return null;
|
|
693
|
+
if (next === out2) break;
|
|
694
|
+
out2 = next;
|
|
695
|
+
}
|
|
696
|
+
return out2;
|
|
697
|
+
};
|
|
698
|
+
const tokens = {};
|
|
699
|
+
const warnings = [];
|
|
700
|
+
for (const [name, raw] of vars) {
|
|
701
|
+
if (!/^--sm-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) continue;
|
|
702
|
+
const written = inline(raw);
|
|
703
|
+
const value = written === null ? null : tokenValue(written);
|
|
704
|
+
if (value === null) warnings.push(`${name}: "${raw.length > 60 ? raw.slice(0, 57) + "\u2026" : raw}" is not a value Cauthy takes`);
|
|
705
|
+
else tokens[name] = value;
|
|
706
|
+
}
|
|
707
|
+
if (Object.keys(tokens).length === 0) warnings.push("no --sm-* token was found in these stylesheets");
|
|
708
|
+
return { tokens, warnings };
|
|
709
|
+
}
|
|
710
|
+
function resolve(value, vars, seen = 0) {
|
|
711
|
+
if (seen > 16) return null;
|
|
712
|
+
let unresolved = false;
|
|
713
|
+
let out2 = value;
|
|
714
|
+
for (let guard = 0; guard < 32 && /var\(/.test(out2); guard++) {
|
|
715
|
+
out2 = out2.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
|
|
716
|
+
const known = vars.get(name);
|
|
717
|
+
const next = known !== void 0 ? resolve(known, vars, seen + 1) : fallback !== void 0 ? resolve(fallback.trim(), vars, seen + 1) : null;
|
|
718
|
+
if (next === null) unresolved = true;
|
|
719
|
+
return next ?? "";
|
|
720
|
+
});
|
|
721
|
+
if (unresolved) return null;
|
|
722
|
+
}
|
|
723
|
+
return /var\(/.test(out2) ? null : out2.trim();
|
|
724
|
+
}
|
|
725
|
+
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
726
|
+
var hex2 = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, "0");
|
|
727
|
+
var gamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
728
|
+
function out(r, g, b, a = 1) {
|
|
729
|
+
if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
|
|
730
|
+
return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1e3) / 1e3})`;
|
|
731
|
+
}
|
|
732
|
+
var alpha = (s) => s === void 0 ? 1 : s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s);
|
|
733
|
+
var KEYWORDS = { white: "#ffffff", black: "#000000" };
|
|
734
|
+
function toColor(value) {
|
|
735
|
+
const v = value.trim().toLowerCase();
|
|
736
|
+
if (KEYWORDS[v]) return KEYWORDS[v];
|
|
737
|
+
if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v;
|
|
738
|
+
let m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
|
|
739
|
+
if (m) {
|
|
740
|
+
const ch = (s) => s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s) / 255;
|
|
741
|
+
return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]));
|
|
742
|
+
}
|
|
743
|
+
m = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
|
|
744
|
+
if (m) {
|
|
745
|
+
const h = (parseFloat(m[1]) % 360 + 360) % 360;
|
|
746
|
+
const s = parseFloat(m[2]) / 100;
|
|
747
|
+
const l = parseFloat(m[3]) / 100;
|
|
748
|
+
const f = (n) => {
|
|
749
|
+
const k = (n + h / 30) % 12;
|
|
750
|
+
return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
751
|
+
};
|
|
752
|
+
return out(f(0), f(8), f(4), alpha(m[4]));
|
|
753
|
+
}
|
|
754
|
+
m = /^oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/.exec(v);
|
|
755
|
+
if (m) {
|
|
756
|
+
const L = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
|
|
757
|
+
const C = m[2].endsWith("%") ? parseFloat(m[2]) / 100 * 0.4 : parseFloat(m[2]);
|
|
758
|
+
const H = parseFloat(m[3]) * Math.PI / 180;
|
|
759
|
+
const a = C * Math.cos(H);
|
|
760
|
+
const b = C * Math.sin(H);
|
|
761
|
+
const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);
|
|
762
|
+
const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);
|
|
763
|
+
const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);
|
|
764
|
+
return out(
|
|
765
|
+
gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),
|
|
766
|
+
gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),
|
|
767
|
+
gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),
|
|
768
|
+
alpha(m[4])
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
function toRadius(value) {
|
|
774
|
+
const m = /^([\d.]+)(px|rem|em)?$/.exec(value.trim());
|
|
775
|
+
if (!m) return null;
|
|
776
|
+
const px = parseFloat(m[1]) * (m[2] === "rem" || m[2] === "em" ? 16 : 1);
|
|
777
|
+
return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null;
|
|
778
|
+
}
|
|
779
|
+
function toFont(value) {
|
|
780
|
+
const v = value.trim();
|
|
781
|
+
return /^[A-Za-z0-9 ,'"_-]{1,200}$/.test(v) && (v.match(/"/g)?.length ?? 0) % 2 === 0 && (v.match(/'/g)?.length ?? 0) % 2 === 0 ? v : null;
|
|
782
|
+
}
|
|
783
|
+
var COLORS = [
|
|
784
|
+
["background", ["--sm-bg-surface", "--sm-surface"]],
|
|
785
|
+
["surface", ["--sm-bg-surface-raised", "--sm-surface-raised"]],
|
|
786
|
+
["text", ["--sm-text-primary", "--sm-surface-text"]],
|
|
787
|
+
["textMuted", ["--sm-text-secondary"]],
|
|
788
|
+
["border", ["--sm-border-default", "--sm-surface-border"]],
|
|
789
|
+
["primary", ["--sm-primary", "--sm-bg-interactive"]],
|
|
790
|
+
["primaryText", ["--sm-primary-text", "--sm-text-inverse"]],
|
|
791
|
+
["accent", ["--sm-text-brand", "--sm-border-focus", "--sm-accent"]],
|
|
792
|
+
["error", ["--sm-error-soft-text", "--sm-error"]],
|
|
793
|
+
["errorBackground", ["--sm-error-soft"]],
|
|
794
|
+
["success", ["--sm-success-soft-text", "--sm-success"]],
|
|
795
|
+
["successBackground", ["--sm-success-soft"]]
|
|
796
|
+
];
|
|
797
|
+
function themeFromStackmyth(css, options = {}) {
|
|
798
|
+
const o = { mode: options.mode ?? "light", theme: options.theme };
|
|
799
|
+
const vars = /* @__PURE__ */ new Map();
|
|
800
|
+
for (const sheet of Array.isArray(css) ? css : [css]) {
|
|
801
|
+
for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value);
|
|
802
|
+
}
|
|
803
|
+
const theme = {};
|
|
804
|
+
const warnings = [];
|
|
805
|
+
const read = (tokens, to, what) => {
|
|
806
|
+
for (const token of tokens) {
|
|
807
|
+
const raw = vars.get(token);
|
|
808
|
+
if (raw === void 0) continue;
|
|
809
|
+
const value = resolve(raw, vars);
|
|
810
|
+
const got = value === null ? null : to(value);
|
|
811
|
+
if (got !== null) return got;
|
|
812
|
+
warnings.push(`${token}: "${raw}" is not ${what} Cauthy can take`);
|
|
813
|
+
}
|
|
814
|
+
return void 0;
|
|
815
|
+
};
|
|
816
|
+
for (const [field, tokens] of COLORS) {
|
|
817
|
+
const c = read(tokens, toColor, "a colour");
|
|
818
|
+
if (c !== void 0) theme[field] = c;
|
|
819
|
+
}
|
|
820
|
+
const radius = read(["--sm-radius-container", "--sm-radius-md"], toRadius, "a length");
|
|
821
|
+
if (radius !== void 0) theme.radius = radius;
|
|
822
|
+
const button = read(["--sm-radius-interactive", "--sm-radius-sm"], toRadius, "a length");
|
|
823
|
+
if (button !== void 0) theme.buttonRadius = button;
|
|
824
|
+
const font = read(["--sm-font-family"], toFont, "a font list");
|
|
825
|
+
if (font !== void 0) theme.fontFamily = font;
|
|
826
|
+
if (options.logoUrl) theme.logoUrl = options.logoUrl;
|
|
827
|
+
if (vars.size === 0) warnings.push("no custom properties were found: is this the stylesheet that declares the --sm-* tokens?");
|
|
828
|
+
else if (![...vars.keys()].some((k) => k.startsWith("--sm-"))) warnings.push("no --sm-* token was found in these stylesheets");
|
|
829
|
+
return { theme, warnings };
|
|
830
|
+
}
|
|
831
|
+
function toWire(theme) {
|
|
832
|
+
const names = {
|
|
833
|
+
background: "background",
|
|
834
|
+
surface: "surface",
|
|
835
|
+
text: "text",
|
|
836
|
+
textMuted: "text_muted",
|
|
837
|
+
border: "border",
|
|
838
|
+
primary: "primary",
|
|
839
|
+
primaryText: "primary_text",
|
|
840
|
+
accent: "accent",
|
|
841
|
+
error: "error",
|
|
842
|
+
errorBackground: "error_background",
|
|
843
|
+
success: "success",
|
|
844
|
+
successBackground: "success_background",
|
|
845
|
+
radius: "radius",
|
|
846
|
+
buttonRadius: "button_radius",
|
|
847
|
+
fontFamily: "font_family",
|
|
848
|
+
logoUrl: "logo_url",
|
|
849
|
+
fonts: "fonts",
|
|
850
|
+
layout: "layout",
|
|
851
|
+
panelColors: "panel_colors",
|
|
852
|
+
panelText: "panel_text",
|
|
853
|
+
panelIconUrl: "panel_icon_url",
|
|
854
|
+
texts: "texts",
|
|
855
|
+
inputBackground: "input_background",
|
|
856
|
+
inputBorder: "input_border",
|
|
857
|
+
tokens: "tokens"
|
|
858
|
+
};
|
|
859
|
+
const wire = {};
|
|
860
|
+
for (const [k, v] of Object.entries(theme)) {
|
|
861
|
+
if (v === void 0 || v === "" || Array.isArray(v) && v.length === 0) continue;
|
|
862
|
+
wire[names[k]] = v;
|
|
863
|
+
}
|
|
864
|
+
return wire;
|
|
865
|
+
}
|
|
866
|
+
function fromWire(wire) {
|
|
867
|
+
const names = {
|
|
868
|
+
background: "background",
|
|
869
|
+
surface: "surface",
|
|
870
|
+
text: "text",
|
|
871
|
+
text_muted: "textMuted",
|
|
872
|
+
border: "border",
|
|
873
|
+
primary: "primary",
|
|
874
|
+
primary_text: "primaryText",
|
|
875
|
+
accent: "accent",
|
|
876
|
+
error: "error",
|
|
877
|
+
error_background: "errorBackground",
|
|
878
|
+
success: "success",
|
|
879
|
+
success_background: "successBackground",
|
|
880
|
+
radius: "radius",
|
|
881
|
+
button_radius: "buttonRadius",
|
|
882
|
+
font_family: "fontFamily",
|
|
883
|
+
logo_url: "logoUrl",
|
|
884
|
+
fonts: "fonts",
|
|
885
|
+
layout: "layout",
|
|
886
|
+
panel_colors: "panelColors",
|
|
887
|
+
panel_text: "panelText",
|
|
888
|
+
panel_icon_url: "panelIconUrl",
|
|
889
|
+
texts: "texts",
|
|
890
|
+
input_background: "inputBackground",
|
|
891
|
+
input_border: "inputBorder",
|
|
892
|
+
tokens: "tokens"
|
|
893
|
+
};
|
|
894
|
+
const theme = {};
|
|
895
|
+
for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
|
|
896
|
+
return theme;
|
|
897
|
+
}
|
|
898
|
+
async function pushTheme(o) {
|
|
899
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
900
|
+
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
901
|
+
const clientId = o.clientId ?? env4("CAUTHY_CLIENT_ID") ?? "";
|
|
902
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
903
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
904
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
905
|
+
if (v === "") throw new Error(`@cauthyhq/node: ${name} is required to push a theme`);
|
|
906
|
+
}
|
|
907
|
+
const res = await deadline(o.fetch, o.timeoutMs ?? DEFAULT_DEADLINE_MS)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {
|
|
908
|
+
method: "PUT",
|
|
909
|
+
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
|
|
910
|
+
body: JSON.stringify(toWire(o.theme))
|
|
911
|
+
});
|
|
912
|
+
if (!res.ok) {
|
|
913
|
+
const body = await res.json().catch(() => null);
|
|
914
|
+
throw new Error(`@cauthyhq/node: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/system.ts
|
|
919
|
+
var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
920
|
+
var SKEW_MS = 3e4;
|
|
921
|
+
var CauthyApiError = class extends Error {
|
|
922
|
+
constructor(status, code, message) {
|
|
923
|
+
super(`@cauthyhq/node: ${message}`);
|
|
924
|
+
this.status = status;
|
|
925
|
+
this.code = code;
|
|
926
|
+
this.name = "CauthyApiError";
|
|
927
|
+
}
|
|
928
|
+
status;
|
|
929
|
+
code;
|
|
930
|
+
};
|
|
931
|
+
function env(name) {
|
|
932
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
933
|
+
}
|
|
934
|
+
function toOrganization(o) {
|
|
935
|
+
return {
|
|
936
|
+
id: o.id,
|
|
937
|
+
system: o.system,
|
|
938
|
+
name: o.name,
|
|
939
|
+
kind: o.kind,
|
|
940
|
+
externalId: o.external_id,
|
|
941
|
+
status: o.status,
|
|
942
|
+
metadata: o.metadata ?? {},
|
|
943
|
+
members: o.members,
|
|
944
|
+
createdAt: o.created_at,
|
|
945
|
+
updatedAt: o.updated_at
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function toMember(m) {
|
|
949
|
+
return { userId: m.user_id, email: m.email, name: m.name, status: m.status, role: m.role, ...m.invited === void 0 ? {} : { invited: m.invited } };
|
|
950
|
+
}
|
|
951
|
+
function asJwk(k) {
|
|
952
|
+
if (typeof k.privateKey !== "string") return k.privateKey;
|
|
953
|
+
return k.privateKey.includes("BEGIN") ? void 0 : JSON.parse(k.privateKey);
|
|
954
|
+
}
|
|
955
|
+
async function loadKey(k) {
|
|
956
|
+
const jwk = asJwk(k);
|
|
957
|
+
return jwk ? importJWK2(jwk, "RS256") : importPKCS8(k.privateKey, "RS256", { extractable: true });
|
|
958
|
+
}
|
|
959
|
+
function createSystem(given = {}) {
|
|
960
|
+
const issuer = withoutTrailingSlashes(given.issuer ?? env("CAUTHY_ISSUER") ?? "");
|
|
961
|
+
const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
|
|
962
|
+
const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
|
|
963
|
+
const envKey = env("CAUTHY_SYSTEM_KEY");
|
|
964
|
+
const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
|
|
965
|
+
const ownSystem = given.system ?? env("CAUTHY_SYSTEM") ?? "";
|
|
966
|
+
const acceptInviteUrl = given.acceptInviteUrl ?? env("CAUTHY_ACCEPT_INVITE_URL") ?? "";
|
|
967
|
+
const inviteHeaders = /^https?:\/\//.test(acceptInviteUrl) ? { "x-cauthy-client-id": clientId, "x-cauthy-link-to": acceptInviteUrl } : {};
|
|
968
|
+
const timeoutMs = given.timeoutMs ?? 2e3;
|
|
969
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CauthyConfigError("timeoutMs must be a positive number of milliseconds");
|
|
970
|
+
const limited = deadline(given.fetch, timeoutMs);
|
|
971
|
+
const doFetch = async (input, init) => {
|
|
972
|
+
try {
|
|
973
|
+
return await limited(input, init);
|
|
974
|
+
} catch (err) {
|
|
975
|
+
if (isTimeout(err)) throw new CauthyApiError(503, "unavailable", `Cauthy did not answer within ${timeoutMs} ms`);
|
|
976
|
+
throw err;
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const tokenEndpoint = `${issuer}/v1/oauth/token`;
|
|
980
|
+
function configured() {
|
|
981
|
+
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
|
|
982
|
+
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
983
|
+
if (given.keys) {
|
|
984
|
+
if (!given.keysClientId) throw new CauthyConfigError("the contract gives the system no identity of its own: its link needs a service block");
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (key ? key.keyId === "" : clientSecret === "") {
|
|
988
|
+
throw new CauthyConfigError(key ? "the key needs its id (CAUTHY_SYSTEM_KEY_ID)" : "to act on its own the application needs its secret (CAUTHY_CLIENT_SECRET) or a key (CAUTHY_SYSTEM_KEY)");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
let imported;
|
|
992
|
+
const signingKey = () => imported ??= loadKey(key);
|
|
993
|
+
async function fail(res, what) {
|
|
994
|
+
const body = await res.json().catch(() => null);
|
|
995
|
+
const first = body?.errors?.[0];
|
|
996
|
+
const code = typeof body?.error === "string" ? body.error : first?.code;
|
|
997
|
+
const message = typeof body?.error === "string" ? body.error_description : first?.message;
|
|
998
|
+
throw new CauthyApiError(res.status, code ?? "error", `Cauthy refused ${what} (${res.status}): ${message ?? res.statusText}`);
|
|
999
|
+
}
|
|
1000
|
+
let held;
|
|
1001
|
+
let asking;
|
|
1002
|
+
async function ask() {
|
|
1003
|
+
configured();
|
|
1004
|
+
const self = given.keys ? given.keysClientId : key?.clientId || clientId;
|
|
1005
|
+
const form = new URLSearchParams({ grant_type: "client_credentials", client_id: self });
|
|
1006
|
+
if (given.keys || key) {
|
|
1007
|
+
const signer = given.keys ? await given.keys.current() : { kid: key.keyId, privateKey: await signingKey() };
|
|
1008
|
+
const assertion = await new SignJWT({}).setProtectedHeader({ alg: "RS256", kid: signer.kid }).setIssuer(self).setSubject(self).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(signer.privateKey);
|
|
1009
|
+
form.set("client_assertion_type", ASSERTION_TYPE);
|
|
1010
|
+
form.set("client_assertion", assertion);
|
|
1011
|
+
} else {
|
|
1012
|
+
form.set("client_secret", clientSecret);
|
|
1013
|
+
}
|
|
1014
|
+
const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
|
|
1015
|
+
if (!res.ok) await fail(res, "the application's own token");
|
|
1016
|
+
const t = await res.json();
|
|
1017
|
+
held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
|
|
1018
|
+
return t.access_token;
|
|
1019
|
+
}
|
|
1020
|
+
function token() {
|
|
1021
|
+
if (held && Date.now() < held.until) return Promise.resolve(held.value);
|
|
1022
|
+
return asking ??= ask().finally(() => {
|
|
1023
|
+
asking = void 0;
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
async function call(method, path, body, extra = {}) {
|
|
1027
|
+
const send = async () => doFetch(`${issuer}${path}`, {
|
|
1028
|
+
method,
|
|
1029
|
+
headers: { authorization: `Bearer ${await token()}`, ...extra, ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
1030
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1031
|
+
});
|
|
1032
|
+
let res = await send();
|
|
1033
|
+
if (res.status === 401 && held) {
|
|
1034
|
+
held = void 0;
|
|
1035
|
+
res = await send();
|
|
1036
|
+
}
|
|
1037
|
+
if (!res.ok) await fail(res, `${method} ${path}`);
|
|
1038
|
+
return res.status === 204 ? void 0 : res.json();
|
|
1039
|
+
}
|
|
1040
|
+
const data = async (method, path, body, extra) => (await call(method, path, body, extra))?.data;
|
|
1041
|
+
const id = encodeURIComponent;
|
|
1042
|
+
return {
|
|
1043
|
+
token,
|
|
1044
|
+
request: data,
|
|
1045
|
+
organizations: {
|
|
1046
|
+
async list(query = {}) {
|
|
1047
|
+
const q = new URLSearchParams();
|
|
1048
|
+
for (const [k, v] of Object.entries(query)) if (v !== void 0) q.set(k, String(v));
|
|
1049
|
+
const out2 = await call("GET", `/v1/organizations${q.size ? `?${q}` : ""}`);
|
|
1050
|
+
return { items: out2.data.map(toOrganization), total: out2.total_count };
|
|
1051
|
+
},
|
|
1052
|
+
async create(input) {
|
|
1053
|
+
return toOrganization(await data(
|
|
1054
|
+
"POST",
|
|
1055
|
+
"/v1/organizations",
|
|
1056
|
+
{ name: input.name, kind: input.kind, external_id: input.externalId, metadata: input.metadata }
|
|
1057
|
+
));
|
|
1058
|
+
},
|
|
1059
|
+
async get(orgId) {
|
|
1060
|
+
return toOrganization(await data("GET", `/v1/organizations/${id(orgId)}`));
|
|
1061
|
+
},
|
|
1062
|
+
async update(orgId, patch) {
|
|
1063
|
+
return toOrganization(await data("PATCH", `/v1/organizations/${id(orgId)}`, patch));
|
|
1064
|
+
},
|
|
1065
|
+
async remove(orgId) {
|
|
1066
|
+
await call("DELETE", `/v1/organizations/${id(orgId)}`);
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
members: {
|
|
1070
|
+
async list(orgId) {
|
|
1071
|
+
return (await data("GET", `/v1/organizations/${id(orgId)}/members`)).map(toMember);
|
|
1072
|
+
},
|
|
1073
|
+
async add(orgId, m) {
|
|
1074
|
+
return toMember(await data(
|
|
1075
|
+
"POST",
|
|
1076
|
+
`/v1/organizations/${id(orgId)}/members`,
|
|
1077
|
+
{ role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId, only_new: m.onlyNew !== false },
|
|
1078
|
+
inviteHeaders
|
|
1079
|
+
));
|
|
1080
|
+
},
|
|
1081
|
+
async setRole(orgId, userId, role) {
|
|
1082
|
+
return toMember(await data("PUT", `/v1/organizations/${id(orgId)}/members/${id(userId)}`, { role }));
|
|
1083
|
+
},
|
|
1084
|
+
async remove(orgId, userId) {
|
|
1085
|
+
await call("DELETE", `/v1/organizations/${id(orgId)}/members/${id(userId)}`);
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
async invite(person) {
|
|
1089
|
+
const u = await data("POST", "/v1/users", { ...person, client_id: clientId }, inviteHeaders);
|
|
1090
|
+
return { id: u.id, email: u.email };
|
|
1091
|
+
},
|
|
1092
|
+
theme: {
|
|
1093
|
+
async get(of = clientId) {
|
|
1094
|
+
return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
policy: {
|
|
1098
|
+
async get(system) {
|
|
1099
|
+
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
catalog: {
|
|
1103
|
+
async get(system) {
|
|
1104
|
+
return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
|
|
1105
|
+
},
|
|
1106
|
+
async set(system, resources) {
|
|
1107
|
+
return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
async check(q) {
|
|
1111
|
+
const system = q.system ?? ownSystem;
|
|
1112
|
+
if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
|
|
1113
|
+
return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
|
|
1114
|
+
},
|
|
1115
|
+
async publicJwks() {
|
|
1116
|
+
if (given.keys) return given.keys.published();
|
|
1117
|
+
if (!key) return { keys: [] };
|
|
1118
|
+
const { kty, n, e } = asJwk(key) ?? await exportJWK2(await signingKey());
|
|
1119
|
+
return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/validate.ts
|
|
1125
|
+
var MIN_PASSWORD = 12;
|
|
1126
|
+
var MAX_PASSWORD = 128;
|
|
1127
|
+
var FLOOR_PASSWORD = 8;
|
|
1128
|
+
var MAX_NAME = 120;
|
|
1129
|
+
var CLASSES = ["lowercase", "uppercase", "digit", "symbol"];
|
|
1130
|
+
var DEFAULT_PASSWORD_POLICY = Object.freeze({ min: MIN_PASSWORD, max: MAX_PASSWORD, requires: [], min_classes: 0, reject_breached: false });
|
|
1131
|
+
var int = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : fallback;
|
|
1132
|
+
function passwordPolicyOf(raw) {
|
|
1133
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
1134
|
+
const min = Math.min(Math.max(int(r.min ?? r.min_length, MIN_PASSWORD) || MIN_PASSWORD, FLOOR_PASSWORD), MAX_PASSWORD);
|
|
1135
|
+
const max = Math.max(Math.min(int(r.max ?? r.max_length, MAX_PASSWORD) || MAX_PASSWORD, MAX_PASSWORD), min);
|
|
1136
|
+
const asked = Array.isArray(r.requires) ? r.requires : [];
|
|
1137
|
+
return {
|
|
1138
|
+
min,
|
|
1139
|
+
max,
|
|
1140
|
+
requires: CLASSES.filter((c) => asked.includes(c)),
|
|
1141
|
+
min_classes: Math.min(Math.max(int(r.min_classes ?? r.minClasses, 0), 0), CLASSES.length),
|
|
1142
|
+
reject_breached: (r.reject_breached ?? r.rejectBreached) === true
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function utf8Length(v) {
|
|
1146
|
+
let n = 0;
|
|
1147
|
+
for (const ch of v) {
|
|
1148
|
+
const c = ch.codePointAt(0);
|
|
1149
|
+
n += c < 128 ? 1 : c < 2048 ? 2 : c < 65536 ? 3 : 4;
|
|
1150
|
+
}
|
|
1151
|
+
return n;
|
|
1152
|
+
}
|
|
1153
|
+
function classesOf(v) {
|
|
1154
|
+
const has = /* @__PURE__ */ new Set();
|
|
1155
|
+
for (const ch of v) {
|
|
1156
|
+
if (new RegExp("\\p{Ll}", "u").test(ch)) has.add("lowercase");
|
|
1157
|
+
else if (new RegExp("\\p{Lu}", "u").test(ch)) has.add("uppercase");
|
|
1158
|
+
else if (new RegExp("\\p{Nd}", "u").test(ch)) has.add("digit");
|
|
1159
|
+
else if (!new RegExp("\\p{L}", "u").test(ch)) has.add("symbol");
|
|
1160
|
+
}
|
|
1161
|
+
return has;
|
|
1162
|
+
}
|
|
1163
|
+
function passwordProblems(value, policy = DEFAULT_PASSWORD_POLICY) {
|
|
1164
|
+
const p = passwordPolicyOf(policy);
|
|
1165
|
+
const out2 = [];
|
|
1166
|
+
const length = utf8Length(value);
|
|
1167
|
+
if (length < p.min) out2.push("too_short");
|
|
1168
|
+
if (length > p.max) out2.push("too_long");
|
|
1169
|
+
const has = classesOf(value);
|
|
1170
|
+
for (const c of p.requires) if (!has.has(c)) out2.push(`needs_${c}`);
|
|
1171
|
+
if (p.min_classes > 0 && CLASSES.filter((c) => has.has(c)).length < p.min_classes) out2.push("needs_classes");
|
|
1172
|
+
return out2;
|
|
1173
|
+
}
|
|
1174
|
+
var RULES = {
|
|
1175
|
+
en: {
|
|
1176
|
+
between: "Between {min} and {max} characters.",
|
|
1177
|
+
requires: "Include {kinds}.",
|
|
1178
|
+
and: "and",
|
|
1179
|
+
classes: "Use at least {n} of these: lowercase letters, uppercase letters, digits, symbols.",
|
|
1180
|
+
breached: "Passwords found in known data breaches are not accepted.",
|
|
1181
|
+
kinds: { lowercase: "a lowercase letter", uppercase: "an uppercase letter", digit: "a digit", symbol: "a symbol" },
|
|
1182
|
+
reasons: {
|
|
1183
|
+
too_short: "Use at least {min} characters.",
|
|
1184
|
+
too_long: "Use at most {max} characters.",
|
|
1185
|
+
needs_lowercase: "Include a lowercase letter.",
|
|
1186
|
+
needs_uppercase: "Include an uppercase letter.",
|
|
1187
|
+
needs_digit: "Include a digit.",
|
|
1188
|
+
needs_symbol: "Include a symbol.",
|
|
1189
|
+
needs_classes: "Use at least {n} of these: lowercase letters, uppercase letters, digits, symbols.",
|
|
1190
|
+
breached: "This password appears in a known data breach. Choose a different one."
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
es: {
|
|
1194
|
+
between: "Entre {min} y {max} caracteres.",
|
|
1195
|
+
requires: "Incluye {kinds}.",
|
|
1196
|
+
and: "y",
|
|
1197
|
+
classes: "Usa al menos {n} de estos tipos: min\xFAsculas, may\xFAsculas, n\xFAmeros, s\xEDmbolos.",
|
|
1198
|
+
breached: "No se aceptan contrase\xF1as que aparecen en filtraciones de datos conocidas.",
|
|
1199
|
+
kinds: { lowercase: "una min\xFAscula", uppercase: "una may\xFAscula", digit: "un n\xFAmero", symbol: "un s\xEDmbolo" },
|
|
1200
|
+
reasons: {
|
|
1201
|
+
too_short: "Usa al menos {min} caracteres.",
|
|
1202
|
+
too_long: "Usa como m\xE1ximo {max} caracteres.",
|
|
1203
|
+
needs_lowercase: "Incluye una min\xFAscula.",
|
|
1204
|
+
needs_uppercase: "Incluye una may\xFAscula.",
|
|
1205
|
+
needs_digit: "Incluye un n\xFAmero.",
|
|
1206
|
+
needs_symbol: "Incluye un s\xEDmbolo.",
|
|
1207
|
+
needs_classes: "Usa al menos {n} de estos tipos: min\xFAsculas, may\xFAsculas, n\xFAmeros, s\xEDmbolos.",
|
|
1208
|
+
breached: "Esta contrase\xF1a aparece en una filtraci\xF3n de datos conocida. Elige otra."
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
var fill = (s, p) => s.replace("{min}", String(p.min)).replace("{max}", String(p.max)).replace("{n}", String(p.min_classes));
|
|
1213
|
+
function describePasswordPolicy(policy = DEFAULT_PASSWORD_POLICY, locale = "en") {
|
|
1214
|
+
const p = passwordPolicyOf(policy);
|
|
1215
|
+
const t = RULES[locale] ?? RULES.en;
|
|
1216
|
+
const parts = [fill(t.between, p)];
|
|
1217
|
+
if (p.requires.length > 0) {
|
|
1218
|
+
const kinds = p.requires.map((c) => t.kinds[c]);
|
|
1219
|
+
const listed = kinds.length === 1 ? kinds[0] : `${kinds.slice(0, -1).join(", ")} ${t.and} ${kinds[kinds.length - 1]}`;
|
|
1220
|
+
parts.push(t.requires.replace("{kinds}", listed));
|
|
1221
|
+
}
|
|
1222
|
+
if (p.min_classes > 0) parts.push(fill(t.classes, p));
|
|
1223
|
+
if (p.reject_breached) parts.push(t.breached);
|
|
1224
|
+
return parts.join(" ");
|
|
1225
|
+
}
|
|
1226
|
+
function explainPasswordProblems(reasons, policy = DEFAULT_PASSWORD_POLICY, locale = "en") {
|
|
1227
|
+
const p = passwordPolicyOf(policy);
|
|
1228
|
+
const t = RULES[locale] ?? RULES.en;
|
|
1229
|
+
const said = reasons.filter((r) => r in t.reasons).map((r) => fill(t.reasons[r], p));
|
|
1230
|
+
return said.length > 0 ? said.join(" ") : describePasswordPolicy(p, locale);
|
|
1231
|
+
}
|
|
1232
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1233
|
+
function checkEmail(value) {
|
|
1234
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
1235
|
+
if (v === "") return { field: "email", code: "required", message: "Enter your email address." };
|
|
1236
|
+
if (v.length > 254 || !EMAIL.test(v)) return { field: "email", code: "invalid_email", message: "That does not look like an email address." };
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
function checkPassword(value) {
|
|
1240
|
+
if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
|
|
1241
|
+
if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
function checkNewPassword(value, policy) {
|
|
1245
|
+
if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
|
|
1246
|
+
const p = policy ? passwordPolicyOf(policy) : DEFAULT_PASSWORD_POLICY;
|
|
1247
|
+
const reasons = passwordProblems(value, p);
|
|
1248
|
+
if (reasons.length === 0) return null;
|
|
1249
|
+
return { field: "password", code: reasons[0], message: explainPasswordProblems(reasons, p) };
|
|
1250
|
+
}
|
|
1251
|
+
function checkName(value) {
|
|
1252
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
1253
|
+
if (typeof value !== "string" || value.trim().length > MAX_NAME) return { field: "name", code: "too_long", message: `Use at most ${MAX_NAME} characters.` };
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
var MAX_ORGANIZATION = 120;
|
|
1257
|
+
function checkOrganization(value, required = false) {
|
|
1258
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
1259
|
+
if (v === "") return required ? { field: "organization", code: "required", message: "Enter your company's name." } : null;
|
|
1260
|
+
if (v.length > MAX_ORGANIZATION) return { field: "organization", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
function checkOrganizationName(value) {
|
|
1264
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
1265
|
+
if (v === "") return { field: "name", code: "required", message: "Enter the organization's name." };
|
|
1266
|
+
if (v.length > MAX_ORGANIZATION) return { field: "name", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
function checkCode(value) {
|
|
1270
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
1271
|
+
if (v === "") return { field: "code", code: "required", message: "Enter the code." };
|
|
1272
|
+
if (v.length > 40) return { field: "code", code: "too_long", message: "That is not a code." };
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
function firstProblem(...checks) {
|
|
1276
|
+
return checks.find((c) => c !== null) ?? null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/webhook.ts
|
|
1280
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
1281
|
+
var CauthyWebhookError = class extends Error {
|
|
1282
|
+
constructor(reason) {
|
|
1283
|
+
super(`@cauthyhq/node: webhook refused: ${reason}`);
|
|
1284
|
+
this.reason = reason;
|
|
1285
|
+
this.name = "CauthyWebhookError";
|
|
1286
|
+
}
|
|
1287
|
+
reason;
|
|
1288
|
+
};
|
|
1289
|
+
function memoryReplayStore(now = Date.now) {
|
|
1290
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1291
|
+
return {
|
|
1292
|
+
async claim(key, until) {
|
|
1293
|
+
const t = now();
|
|
1294
|
+
if (seen.size > 1e4) {
|
|
1295
|
+
for (const [k, u] of seen) if (u <= t) seen.delete(k);
|
|
1296
|
+
}
|
|
1297
|
+
const held = seen.get(key);
|
|
1298
|
+
if (held !== void 0 && held > t) return false;
|
|
1299
|
+
seen.set(key, until.getTime());
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
var keys = /* @__PURE__ */ new Map();
|
|
1305
|
+
function keysOf(issuer) {
|
|
1306
|
+
let set = keys.get(issuer);
|
|
1307
|
+
if (set === void 0) {
|
|
1308
|
+
set = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
|
|
1309
|
+
keys.set(issuer, set);
|
|
1310
|
+
}
|
|
1311
|
+
return set;
|
|
1312
|
+
}
|
|
1313
|
+
var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
|
|
1314
|
+
async function verifyWebhook(request, options = {}) {
|
|
1315
|
+
const env4 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
|
|
1316
|
+
const issuer = withoutTrailingSlashes(options.issuer ?? env4 ?? "");
|
|
1317
|
+
if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
|
|
1318
|
+
const signature = request.headers.get("x-cauthy-signature-jwt");
|
|
1319
|
+
if (!signature) throw new CauthyWebhookError("no signature");
|
|
1320
|
+
const raw = new Uint8Array(await request.arrayBuffer());
|
|
1321
|
+
const audience = options.url ?? request.url.split("?")[0];
|
|
1322
|
+
let claims;
|
|
1323
|
+
try {
|
|
1324
|
+
const verified = await jwtVerify(signature, options.jwks ?? keysOf(issuer), {
|
|
1325
|
+
algorithms: ["RS256"],
|
|
1326
|
+
audience,
|
|
1327
|
+
typ: "cauthy-webhook+jwt",
|
|
1328
|
+
requiredClaims: ["exp", "iat", "jti", "body_sha256"],
|
|
1329
|
+
currentDate: options.now,
|
|
1330
|
+
clockTolerance: 30
|
|
1331
|
+
});
|
|
1332
|
+
claims = verified.payload;
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
throw new CauthyWebhookError(err instanceof Error ? err.message : "bad signature");
|
|
1335
|
+
}
|
|
1336
|
+
if (claims.iss !== issuer && claims.domain_iss !== issuer) {
|
|
1337
|
+
throw new CauthyWebhookError('unexpected "iss" claim value');
|
|
1338
|
+
}
|
|
1339
|
+
const now = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
1340
|
+
if (typeof claims.iat !== "number" || claims.iat > now + 30) throw new CauthyWebhookError("signed in the future");
|
|
1341
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", raw));
|
|
1342
|
+
let b64 = "";
|
|
1343
|
+
for (const b of digest) b64 += String.fromCharCode(b);
|
|
1344
|
+
const hash = btoa(b64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1345
|
+
if (hash !== claims.body_sha256) throw new CauthyWebhookError("the body is not the one that was signed");
|
|
1346
|
+
let body;
|
|
1347
|
+
try {
|
|
1348
|
+
body = JSON.parse(new TextDecoder().decode(raw));
|
|
1349
|
+
} catch {
|
|
1350
|
+
throw new CauthyWebhookError("the body is not JSON");
|
|
1351
|
+
}
|
|
1352
|
+
if (body.id !== claims.jti || body.event !== claims.evt || body.tenant_id !== claims.tid) {
|
|
1353
|
+
throw new CauthyWebhookError("the body and its signature disagree");
|
|
1354
|
+
}
|
|
1355
|
+
if (options.replay) {
|
|
1356
|
+
const exp = typeof claims.exp === "number" ? claims.exp : now + 300;
|
|
1357
|
+
if (!await options.replay.claim(`${String(claims.jti)}:${String(claims.iat)}`, new Date(exp * 1e3))) {
|
|
1358
|
+
throw new CauthyWebhookError("this delivery was already taken");
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const inner = body.data ?? {};
|
|
1362
|
+
return {
|
|
1363
|
+
id: String(body.id),
|
|
1364
|
+
type: String(body.event),
|
|
1365
|
+
tenantId: String(body.tenant_id),
|
|
1366
|
+
createdAt: String(inner.created_at ?? body.created_at ?? ""),
|
|
1367
|
+
...hex(inner.user_id) ? { userId: inner.user_id } : {},
|
|
1368
|
+
...hex(inner.actor_id) ? { actorId: inner.actor_id } : {},
|
|
1369
|
+
data: inner.data ?? {}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/protocol.ts
|
|
1374
|
+
var PROTOCOL_VERSION = "1";
|
|
1375
|
+
var SIGN_IN_ROUTES = /* @__PURE__ */ new Set([
|
|
1376
|
+
"/sign-in/email",
|
|
1377
|
+
"/sign-in/mfa",
|
|
1378
|
+
"/sign-in/mfa/send",
|
|
1379
|
+
"/sign-in/mfa/email",
|
|
1380
|
+
"/sign-in/mfa/push",
|
|
1381
|
+
"/sign-in/mfa/push/finish",
|
|
1382
|
+
"/sign-up/email",
|
|
1383
|
+
"/invitation/accept",
|
|
1384
|
+
"/sign-in/magic-link",
|
|
1385
|
+
"/sign-in/email-code",
|
|
1386
|
+
"/sign-in/magic-link/verify",
|
|
1387
|
+
"/sign-in/passkey/begin",
|
|
1388
|
+
"/sign-in/passkey/finish",
|
|
1389
|
+
"/sign-in/google-one-tap/begin",
|
|
1390
|
+
"/sign-in/google-one-tap",
|
|
1391
|
+
"/sign-in/anonymous",
|
|
1392
|
+
"/me/upgrade"
|
|
1393
|
+
]);
|
|
1394
|
+
function nextOf(route, status) {
|
|
1395
|
+
switch (status) {
|
|
1396
|
+
case "signed_in":
|
|
1397
|
+
return "done";
|
|
1398
|
+
case "mfa_required":
|
|
1399
|
+
return "mfa";
|
|
1400
|
+
case "verification_required":
|
|
1401
|
+
return "verify_email";
|
|
1402
|
+
// A code went out: the second step's, or the one that comes with a sign-in link.
|
|
1403
|
+
case "sent":
|
|
1404
|
+
return route.startsWith("/sign-in/mfa") ? "mfa" : "code";
|
|
1405
|
+
// A ceremony began: the page shows Google's prompt and posts what it gives, or runs the passkey's.
|
|
1406
|
+
case "started":
|
|
1407
|
+
return route === "/sign-in/google-one-tap/begin" ? "one_tap" : "passkey";
|
|
1408
|
+
default:
|
|
1409
|
+
return void 0;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async function finish(res, route, cacheable = false) {
|
|
1413
|
+
const headers = new Headers();
|
|
1414
|
+
res.headers.forEach((value, name) => {
|
|
1415
|
+
if (name !== "set-cookie") headers.set(name, value);
|
|
1416
|
+
});
|
|
1417
|
+
for (const cookie of res.headers.getSetCookie()) headers.append("set-cookie", cookie);
|
|
1418
|
+
headers.set("cauthy-protocol", PROTOCOL_VERSION);
|
|
1419
|
+
if (!cacheable && !(headers.get("cache-control") ?? "").includes("no-store")) headers.set("cache-control", "no-store");
|
|
1420
|
+
let body = res.body;
|
|
1421
|
+
if (route !== null && SIGN_IN_ROUTES.has(route) && res.ok && (headers.get("content-type") ?? "").startsWith("application/json")) {
|
|
1422
|
+
const text2 = await res.text();
|
|
1423
|
+
body = text2;
|
|
1424
|
+
try {
|
|
1425
|
+
const parsed = JSON.parse(text2);
|
|
1426
|
+
const next = nextOf(route, parsed?.status);
|
|
1427
|
+
if (parsed !== null && next !== void 0) body = JSON.stringify({ ...parsed, next });
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
headers.delete("content-length");
|
|
1431
|
+
}
|
|
1432
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers });
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/verify.ts
|
|
1436
|
+
import { compactVerify, createLocalJWKSet } from "jose";
|
|
1437
|
+
|
|
1438
|
+
// src/json.ts
|
|
1439
|
+
var SPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
|
|
1440
|
+
var LIMIT = 2n ** 53n;
|
|
1441
|
+
function parseStrict(text2, integerMembers = []) {
|
|
1442
|
+
let i = 0;
|
|
1443
|
+
let depth = 0;
|
|
1444
|
+
const fail = (what) => {
|
|
1445
|
+
throw new SyntaxError(`${what} at ${i}`);
|
|
1446
|
+
};
|
|
1447
|
+
const skip = () => {
|
|
1448
|
+
while (i < text2.length && SPACE.has(text2[i])) i++;
|
|
1449
|
+
};
|
|
1450
|
+
const value = (integer = false) => {
|
|
1451
|
+
skip();
|
|
1452
|
+
const c = text2[i];
|
|
1453
|
+
if (c === "{") return object();
|
|
1454
|
+
if (c === "[") return array();
|
|
1455
|
+
if (c === '"') return string();
|
|
1456
|
+
if (c === "-" || c >= "0" && c <= "9") return number(integer);
|
|
1457
|
+
for (const [word, v] of [["true", true], ["false", false], ["null", null]]) {
|
|
1458
|
+
if (text2.startsWith(word, i)) {
|
|
1459
|
+
i += word.length;
|
|
1460
|
+
return v;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return fail("unexpected character");
|
|
1464
|
+
};
|
|
1465
|
+
const string = () => {
|
|
1466
|
+
const start = i;
|
|
1467
|
+
i++;
|
|
1468
|
+
while (i < text2.length && text2[i] !== '"') i += text2[i] === "\\" ? 2 : 1;
|
|
1469
|
+
if (text2[i] !== '"') fail("unterminated string");
|
|
1470
|
+
i++;
|
|
1471
|
+
return JSON.parse(text2.slice(start, i));
|
|
1472
|
+
};
|
|
1473
|
+
const number = (integer) => {
|
|
1474
|
+
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text2.slice(i));
|
|
1475
|
+
if (!m) return fail("bad number");
|
|
1476
|
+
i += m[0].length;
|
|
1477
|
+
if (integer && (m[2] !== void 0 || m[3] !== void 0)) return Number.NaN;
|
|
1478
|
+
if (m[2] === void 0 && m[3] === void 0) {
|
|
1479
|
+
const big = BigInt(m[0]);
|
|
1480
|
+
if (big > LIMIT || big < -LIMIT) return Number.NaN;
|
|
1481
|
+
}
|
|
1482
|
+
return Number(m[0]);
|
|
1483
|
+
};
|
|
1484
|
+
const array = () => {
|
|
1485
|
+
i++;
|
|
1486
|
+
depth++;
|
|
1487
|
+
const out2 = [];
|
|
1488
|
+
skip();
|
|
1489
|
+
if (text2[i] === "]") {
|
|
1490
|
+
i++;
|
|
1491
|
+
depth--;
|
|
1492
|
+
return out2;
|
|
1493
|
+
}
|
|
1494
|
+
for (; ; ) {
|
|
1495
|
+
out2.push(value());
|
|
1496
|
+
skip();
|
|
1497
|
+
if (text2[i] === ",") i++;
|
|
1498
|
+
else if (text2[i] === "]") {
|
|
1499
|
+
i++;
|
|
1500
|
+
depth--;
|
|
1501
|
+
return out2;
|
|
1502
|
+
} else fail("expected , or ]");
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
const object = () => {
|
|
1506
|
+
i++;
|
|
1507
|
+
depth++;
|
|
1508
|
+
const out2 = {};
|
|
1509
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1510
|
+
skip();
|
|
1511
|
+
if (text2[i] === "}") {
|
|
1512
|
+
i++;
|
|
1513
|
+
depth--;
|
|
1514
|
+
return out2;
|
|
1515
|
+
}
|
|
1516
|
+
for (; ; ) {
|
|
1517
|
+
skip();
|
|
1518
|
+
if (text2[i] !== '"') fail("expected a name");
|
|
1519
|
+
const name = string();
|
|
1520
|
+
if (seen.has(name)) fail(`repeated name ${JSON.stringify(name)}`);
|
|
1521
|
+
seen.add(name);
|
|
1522
|
+
skip();
|
|
1523
|
+
if (text2[i] !== ":") fail("expected :");
|
|
1524
|
+
i++;
|
|
1525
|
+
Object.defineProperty(out2, name, { value: value(depth === 1 && integerMembers.includes(name)), enumerable: true, writable: true, configurable: true });
|
|
1526
|
+
skip();
|
|
1527
|
+
if (text2[i] === ",") i++;
|
|
1528
|
+
else if (text2[i] === "}") {
|
|
1529
|
+
i++;
|
|
1530
|
+
depth--;
|
|
1531
|
+
return out2;
|
|
1532
|
+
} else fail("expected , or }");
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
const result = value();
|
|
1536
|
+
skip();
|
|
1537
|
+
if (i < text2.length) fail("trailing characters");
|
|
1538
|
+
return result;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/verify.ts
|
|
1542
|
+
var CauthyUnavailableError = class extends Error {
|
|
1543
|
+
constructor(detail) {
|
|
1544
|
+
super(`@cauthyhq/node: Cauthy's keys are unavailable: ${detail}`);
|
|
1545
|
+
this.name = "CauthyUnavailableError";
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
var CauthyTokenError = class extends Error {
|
|
1549
|
+
constructor(reason, detail) {
|
|
1550
|
+
super(`@cauthyhq/node: token refused: ${reason}${detail ? ` (${detail})` : ""}`);
|
|
1551
|
+
this.reason = reason;
|
|
1552
|
+
this.detail = detail;
|
|
1553
|
+
this.name = "CauthyTokenError";
|
|
1554
|
+
}
|
|
1555
|
+
reason;
|
|
1556
|
+
detail;
|
|
1557
|
+
};
|
|
1558
|
+
var MAX_TOKEN_BYTES = 8192;
|
|
1559
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1560
|
+
var NIL_UUID2 = "00000000-0000-0000-0000-000000000000";
|
|
1561
|
+
var SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
1562
|
+
var MAX_KEYS_BYTES = 64 * 1024;
|
|
1563
|
+
var KEYS_TIMEOUT_MS = 5e3;
|
|
1564
|
+
var KEYS_COOLDOWN_MS = 3e4;
|
|
1565
|
+
var KEYS_DEFAULT_TTL_S = 300;
|
|
1566
|
+
var KEYS_MIN_TTL_S = 60;
|
|
1567
|
+
var KEYS_MAX_TTL_S = 3600;
|
|
1568
|
+
function remoteKeys(issuer, fetchKeys = fetch) {
|
|
1569
|
+
let set;
|
|
1570
|
+
let startedAt = 0;
|
|
1571
|
+
let expiresAt = 0;
|
|
1572
|
+
let inflight;
|
|
1573
|
+
const load = async () => {
|
|
1574
|
+
startedAt = Date.now();
|
|
1575
|
+
try {
|
|
1576
|
+
const res = await fetchKeys(`${issuer}/.well-known/jwks.json`, { redirect: "manual", signal: AbortSignal.timeout(KEYS_TIMEOUT_MS), headers: { accept: "application/json" } });
|
|
1577
|
+
if (res.status !== 200) throw new Error(`answered ${res.status}`);
|
|
1578
|
+
const bytes = await readCapped(res, MAX_KEYS_BYTES);
|
|
1579
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
1580
|
+
if (!parsed || !Array.isArray(parsed.keys)) throw new Error("not a key set");
|
|
1581
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1582
|
+
for (const k of parsed.keys) {
|
|
1583
|
+
const usable = k?.kty === "RSA" && (k.use === void 0 || k.use === "sig") && (k.alg === void 0 || k.alg === "RS256");
|
|
1584
|
+
if (!usable || typeof k.kid !== "string") continue;
|
|
1585
|
+
if (seen.has(k.kid)) throw new Error(`two usable keys share the kid ${JSON.stringify(k.kid)}`);
|
|
1586
|
+
seen.add(k.kid);
|
|
1587
|
+
}
|
|
1588
|
+
set = createLocalJWKSet(parsed);
|
|
1589
|
+
const maxAge = /max-age=(\d+)/.exec(res.headers.get("cache-control") ?? "");
|
|
1590
|
+
const ttl = Math.max(KEYS_MIN_TTL_S, Math.min(maxAge ? Number(maxAge[1]) : KEYS_DEFAULT_TTL_S, KEYS_MAX_TTL_S));
|
|
1591
|
+
expiresAt = Date.now() + ttl * 1e3;
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
if (set === void 0) throw new CauthyUnavailableError(err instanceof Error ? err.message : "unreachable");
|
|
1594
|
+
expiresAt = Math.max(expiresAt, Date.now() + KEYS_COOLDOWN_MS);
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
const refresh = () => {
|
|
1598
|
+
inflight ??= load().finally(() => {
|
|
1599
|
+
inflight = void 0;
|
|
1600
|
+
});
|
|
1601
|
+
return inflight;
|
|
1602
|
+
};
|
|
1603
|
+
return async (header, token) => {
|
|
1604
|
+
if (set === void 0 || Date.now() >= expiresAt) await refresh();
|
|
1605
|
+
try {
|
|
1606
|
+
return await set(header, token);
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
const unknown = err?.code === "ERR_JWKS_NO_MATCHING_KEY";
|
|
1609
|
+
if (unknown && Date.now() - startedAt >= KEYS_COOLDOWN_MS) {
|
|
1610
|
+
await refresh();
|
|
1611
|
+
return set(header, token);
|
|
1612
|
+
}
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
async function readCapped(res, max) {
|
|
1618
|
+
const reader = res.body?.getReader();
|
|
1619
|
+
if (!reader) return new Uint8Array();
|
|
1620
|
+
const chunks = [];
|
|
1621
|
+
let size = 0;
|
|
1622
|
+
for (; ; ) {
|
|
1623
|
+
const { done, value } = await reader.read();
|
|
1624
|
+
if (done) break;
|
|
1625
|
+
size += value.length;
|
|
1626
|
+
if (size > max) {
|
|
1627
|
+
await reader.cancel();
|
|
1628
|
+
throw new Error(`over ${max} bytes`);
|
|
1629
|
+
}
|
|
1630
|
+
chunks.push(value);
|
|
1631
|
+
}
|
|
1632
|
+
const out2 = new Uint8Array(size);
|
|
1633
|
+
let at = 0;
|
|
1634
|
+
for (const c of chunks) {
|
|
1635
|
+
out2.set(c, at);
|
|
1636
|
+
at += c.length;
|
|
1637
|
+
}
|
|
1638
|
+
return out2;
|
|
1639
|
+
}
|
|
1640
|
+
var published2 = /* @__PURE__ */ new Map();
|
|
1641
|
+
var withFetch = /* @__PURE__ */ new WeakMap();
|
|
1642
|
+
function remoteKeysFor(issuer, fetchKeys) {
|
|
1643
|
+
let byIssuer = withFetch.get(fetchKeys);
|
|
1644
|
+
if (!byIssuer) withFetch.set(fetchKeys, byIssuer = /* @__PURE__ */ new Map());
|
|
1645
|
+
let set = byIssuer.get(issuer);
|
|
1646
|
+
if (!set) byIssuer.set(issuer, set = remoteKeys(issuer, fetchKeys));
|
|
1647
|
+
return set;
|
|
1648
|
+
}
|
|
1649
|
+
function keysOf2(issuer) {
|
|
1650
|
+
let set = published2.get(issuer);
|
|
1651
|
+
if (set === void 0) {
|
|
1652
|
+
set = remoteKeys(issuer);
|
|
1653
|
+
published2.set(issuer, set);
|
|
1654
|
+
}
|
|
1655
|
+
return set;
|
|
1656
|
+
}
|
|
1657
|
+
function env2(name) {
|
|
1658
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
1659
|
+
}
|
|
1660
|
+
function bearer(from) {
|
|
1661
|
+
if (typeof from === "string") return from.replace(/^Bearer\s+/i, "");
|
|
1662
|
+
const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
|
|
1663
|
+
return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
|
|
1664
|
+
}
|
|
1665
|
+
function issuerAddress(value, what) {
|
|
1666
|
+
let url;
|
|
1667
|
+
try {
|
|
1668
|
+
url = new URL(value);
|
|
1669
|
+
} catch {
|
|
1670
|
+
throw new CauthyConfigError(`${what} must be an https address, e.g. https://id.example.com`);
|
|
1671
|
+
}
|
|
1672
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname.endsWith(".localhost") || url.hostname.endsWith(".test");
|
|
1673
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) throw new CauthyConfigError(`${what} must be https (http only for localhost, *.localhost and *.test)`);
|
|
1674
|
+
if (url.search !== "" || url.hash !== "") throw new CauthyConfigError(`${what} must have no query or fragment`);
|
|
1675
|
+
return withoutTrailingSlashes(value);
|
|
1676
|
+
}
|
|
1677
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1678
|
+
function decodeJson(segment) {
|
|
1679
|
+
const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1680
|
+
return parseStrict(Buffer.from(padded, "base64").toString("utf8"));
|
|
1681
|
+
}
|
|
1682
|
+
function createTokenVerifier(options = {}) {
|
|
1683
|
+
const issuerGiven = options.issuer ?? env2("CAUTHY_ISSUER") ?? "";
|
|
1684
|
+
if (issuerGiven === "") throw new CauthyConfigError("issuer (CAUTHY_ISSUER) is not set");
|
|
1685
|
+
const issuer = issuerAddress(issuerGiven, "issuer");
|
|
1686
|
+
const issuers = [issuer, ...(options.issuers ?? []).map((i) => issuerAddress(i, "issuers"))];
|
|
1687
|
+
const audience = options.audience ?? env2("CAUTHY_AUDIENCE") ?? "";
|
|
1688
|
+
if (audience === "") throw new CauthyConfigError("audience (CAUTHY_AUDIENCE) is not set: without it a token issued for another application would be accepted");
|
|
1689
|
+
const tenant = options.tenantId ?? env2("CAUTHY_TENANT_ID") ?? "";
|
|
1690
|
+
if (tenant === "") throw new CauthyConfigError("tenantId (CAUTHY_TENANT_ID) is not set: without it a token of another workspace would be accepted");
|
|
1691
|
+
if (!UUID2.test(tenant) || tenant === NIL_UUID2) throw new CauthyConfigError("tenantId must be the workspace's id, a lowercase UUID that is not the nil UUID");
|
|
1692
|
+
const leeway = options.leeway ?? 30;
|
|
1693
|
+
if (!Number.isFinite(leeway) || leeway < 0 || leeway > 60) throw new CauthyConfigError("leeway must be 0 to 60 seconds");
|
|
1694
|
+
return async (from) => {
|
|
1695
|
+
const token = bearer(from);
|
|
1696
|
+
if (token === "") throw new CauthyTokenError("no_token");
|
|
1697
|
+
if (Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new CauthyTokenError("too_large");
|
|
1698
|
+
const parts = token.split(".");
|
|
1699
|
+
if (parts.length !== 3 || !parts.every((p) => SEGMENT.test(p) || p === "")) throw new CauthyTokenError("malformed", "not three base64url segments");
|
|
1700
|
+
let header;
|
|
1701
|
+
try {
|
|
1702
|
+
header = decodeJson(parts[0]);
|
|
1703
|
+
} catch {
|
|
1704
|
+
throw new CauthyTokenError("malformed", "the header is not JSON");
|
|
1705
|
+
}
|
|
1706
|
+
if (!isObject(header)) throw new CauthyTokenError("malformed", "the header is not an object");
|
|
1707
|
+
if (header.alg !== "RS256") throw new CauthyTokenError("alg_not_allowed");
|
|
1708
|
+
if ("crit" in header) throw new CauthyTokenError("unsupported_header");
|
|
1709
|
+
if ("typ" in header && !(typeof header.typ === "string" && ["jwt", "at+jwt"].includes(header.typ.toLowerCase()))) {
|
|
1710
|
+
throw new CauthyTokenError("unsupported_header", "typ");
|
|
1711
|
+
}
|
|
1712
|
+
if (typeof header.kid !== "string" || header.kid === "") throw new CauthyTokenError("missing_kid");
|
|
1713
|
+
let payloadBytes;
|
|
1714
|
+
try {
|
|
1715
|
+
const verified = await compactVerify(token, options.jwks ?? (options.fetch ? remoteKeysFor(issuer, options.fetch) : keysOf2(issuer)), { algorithms: ["RS256"] });
|
|
1716
|
+
payloadBytes = verified.payload;
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
const code = err?.code;
|
|
1719
|
+
if (code === "ERR_JWKS_NO_MATCHING_KEY") throw new CauthyTokenError("unknown_kid");
|
|
1720
|
+
if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") throw new CauthyTokenError("bad_signature");
|
|
1721
|
+
if (err instanceof TypeError && /2048/.test(err.message)) throw new CauthyTokenError("weak_key");
|
|
1722
|
+
if (code === "ERR_JWS_INVALID") throw new CauthyTokenError("malformed", err instanceof Error ? err.message : void 0);
|
|
1723
|
+
if (err instanceof CauthyUnavailableError) throw err;
|
|
1724
|
+
throw new CauthyTokenError("bad_signature", err instanceof Error ? err.message : void 0);
|
|
1725
|
+
}
|
|
1726
|
+
let claims;
|
|
1727
|
+
try {
|
|
1728
|
+
claims = parseStrict(new TextDecoder().decode(payloadBytes), ["exp", "iat", "nbf"]);
|
|
1729
|
+
} catch {
|
|
1730
|
+
throw new CauthyTokenError("malformed", "the payload is not JSON");
|
|
1731
|
+
}
|
|
1732
|
+
if (!isObject(claims)) throw new CauthyTokenError("malformed", "the payload is not an object");
|
|
1733
|
+
for (const name of ["iss", "sub", "aud", "exp", "iat", "tid"]) {
|
|
1734
|
+
if (claims[name] === void 0) throw new CauthyTokenError("missing_claim", name);
|
|
1735
|
+
}
|
|
1736
|
+
const bad = (name) => {
|
|
1737
|
+
throw new CauthyTokenError("invalid_claim", name);
|
|
1738
|
+
};
|
|
1739
|
+
if (typeof claims.iss !== "string") bad("iss");
|
|
1740
|
+
if (typeof claims.sub !== "string" || !UUID2.test(claims.sub)) bad("sub");
|
|
1741
|
+
if (typeof claims.tid !== "string" || !UUID2.test(claims.tid)) bad("tid");
|
|
1742
|
+
const aud = claims.aud;
|
|
1743
|
+
if (!(typeof aud === "string" || Array.isArray(aud) && aud.length > 0 && aud.every((a) => typeof a === "string"))) bad("aud");
|
|
1744
|
+
for (const name of ["exp", "iat", "nbf"]) if (claims[name] !== void 0 && !Number.isInteger(claims[name])) bad(name);
|
|
1745
|
+
if (claims.roles !== void 0 && claims.roles !== null && !(Array.isArray(claims.roles) && claims.roles.every((r) => typeof r === "string"))) bad("roles");
|
|
1746
|
+
if (claims.email !== void 0 && typeof claims.email !== "string") bad("email");
|
|
1747
|
+
for (const name of ["email_verified", "anon"]) if (claims[name] !== void 0 && typeof claims[name] !== "boolean") bad(name);
|
|
1748
|
+
for (const name of ["token_use", "azp"]) if (claims[name] !== void 0 && typeof claims[name] !== "string") bad(name);
|
|
1749
|
+
if (!issuers.includes(claims.iss)) throw new CauthyTokenError("wrong_issuer");
|
|
1750
|
+
if (!(Array.isArray(aud) ? aud.includes(audience) : aud === audience)) throw new CauthyTokenError("wrong_audience");
|
|
1751
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3;
|
|
1752
|
+
if (now >= claims.exp + leeway) throw new CauthyTokenError("expired");
|
|
1753
|
+
if (claims.nbf !== void 0 && now < claims.nbf - leeway) throw new CauthyTokenError("not_yet_valid");
|
|
1754
|
+
if (now < claims.iat - leeway) throw new CauthyTokenError("issued_in_future");
|
|
1755
|
+
if (claims.tid !== tenant) throw new CauthyTokenError("wrong_tenant");
|
|
1756
|
+
if ("sys" in claims && !options.allowSystem) throw new CauthyTokenError("system_token");
|
|
1757
|
+
if ("token_use" in claims && !options.allowClientTokens) throw new CauthyTokenError("client_token");
|
|
1758
|
+
if (claims.anon === true && !options.allowAnonymous) throw new CauthyTokenError("anonymous_token");
|
|
1759
|
+
if (options.clientId && typeof claims.azp === "string" && claims.azp !== "" && claims.azp !== options.clientId) throw new CauthyTokenError("wrong_client");
|
|
1760
|
+
return {
|
|
1761
|
+
userId: claims.sub,
|
|
1762
|
+
tenantId: claims.tid,
|
|
1763
|
+
...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
|
|
1764
|
+
...typeof claims.email_verified === "boolean" ? { emailVerified: claims.email_verified } : {},
|
|
1765
|
+
roles: Array.isArray(claims.roles) ? claims.roles : [],
|
|
1766
|
+
...typeof claims.sys === "string" && claims.sys !== "" ? { system: claims.sys } : {},
|
|
1767
|
+
...claims.anon === true ? { anonymous: true } : {},
|
|
1768
|
+
...typeof claims.azp === "string" && claims.azp !== "" ? { authorizedParty: claims.azp } : {},
|
|
1769
|
+
...typeof claims.client_id === "string" && claims.client_id !== "" ? { clientId: claims.client_id } : {},
|
|
1770
|
+
expiresAt: new Date(claims.exp * 1e3).toISOString(),
|
|
1771
|
+
token
|
|
1772
|
+
};
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
async function verifyToken(from, options = {}) {
|
|
1776
|
+
return createTokenVerifier(options)(from);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/deprecate.ts
|
|
1780
|
+
var told = /* @__PURE__ */ new Set();
|
|
1781
|
+
function deprecated(what, instead) {
|
|
1782
|
+
if (told.has(what)) return;
|
|
1783
|
+
told.add(what);
|
|
1784
|
+
const message = `@cauthyhq/node: ${what} is deprecated and leaves in 2.0. ${instead}`;
|
|
1785
|
+
if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
|
|
1786
|
+
process.emitWarning(message, { type: "DeprecationWarning", code: "CAUTHY_DEPRECATED" });
|
|
1787
|
+
} else {
|
|
1788
|
+
console.warn(message);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// src/mount.ts
|
|
1793
|
+
async function checkMount(basePath, target) {
|
|
1794
|
+
const origin = typeof target === "string" ? withoutTrailingSlashes(target) : "http://localhost";
|
|
1795
|
+
if (!/^https?:\/\/[^/\s]+$/.test(origin)) {
|
|
1796
|
+
throw new CauthyConfigError(`checkMount needs the application's origin, e.g. http://localhost:3000, not ${JSON.stringify(target)}`);
|
|
1797
|
+
}
|
|
1798
|
+
const send = typeof target === "string" ? (r) => fetch(r, { redirect: "manual" }) : target;
|
|
1799
|
+
const probes = [
|
|
1800
|
+
new Request(`${origin}${basePath}/session`, { headers: { accept: "application/json" } }),
|
|
1801
|
+
new Request(`${origin}${basePath}/cauthy-mount-check`),
|
|
1802
|
+
new Request(`${origin}${basePath}/sign-out`, { method: "POST", headers: { origin, "content-type": "application/json", "x-cauthy-request": "1" }, body: "{}" })
|
|
1803
|
+
];
|
|
1804
|
+
for (const probe of probes) {
|
|
1805
|
+
const what = `${probe.method} ${new URL(probe.url).pathname}`;
|
|
1806
|
+
let res;
|
|
1807
|
+
try {
|
|
1808
|
+
res = await send(probe);
|
|
1809
|
+
} catch (err) {
|
|
1810
|
+
throw new CauthyConfigError(typeof target === "string" ? `could not reach ${what} to check where the handler is mounted: ${err.message}` : `the handler threw on ${what}: ${err.message}`);
|
|
1811
|
+
}
|
|
1812
|
+
await res.body?.cancel();
|
|
1813
|
+
if (res.headers.get("cauthy-protocol") !== PROTOCOL_VERSION) {
|
|
1814
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1815
|
+
throw new CauthyConfigError(`${what}: the mount redirects (${res.status} to ${res.headers.get("location") ?? "nowhere"}): a proxy rule, a trailing-slash or HTTPS redirect sits in front of ${basePath}. Cauthy is not what answers.`);
|
|
1816
|
+
}
|
|
1817
|
+
if (res.status >= 500) {
|
|
1818
|
+
throw new CauthyConfigError(`${what} answered ${res.status} without Cauthy-Protocol: ${PROTOCOL_VERSION}: the application crashed before or instead of the handler. Look at its log.`);
|
|
1819
|
+
}
|
|
1820
|
+
throw new CauthyConfigError(`${what} answered ${res.status} without Cauthy-Protocol: ${PROTOCOL_VERSION}: something other than this handler answers under ${basePath}. Move that route, mount Cauthy before it, or give Cauthy another basePath.`);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/cauthy.ts
|
|
1826
|
+
var PERSISTENT_SECONDS = 30 * 86400;
|
|
1827
|
+
var FLOW_SECONDS = 3600;
|
|
1828
|
+
var AGAIN_SECONDS = 120;
|
|
1829
|
+
var SKEW_SECONDS = 30;
|
|
1830
|
+
var ORGANIZATIONS_TTL_MS = 6e4;
|
|
1831
|
+
var LOOK_TTL_MS = 6e4;
|
|
1832
|
+
var PROVIDER_ID = /^(custom:)?[a-z0-9_-]{1,40}$/;
|
|
1833
|
+
function providerIn(segment) {
|
|
1834
|
+
try {
|
|
1835
|
+
return decodeURIComponent(segment);
|
|
1836
|
+
} catch {
|
|
1837
|
+
return "";
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function env3(name) {
|
|
1841
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
1842
|
+
}
|
|
1843
|
+
function hostOf(origin) {
|
|
1844
|
+
try {
|
|
1845
|
+
return new URL(origin).hostname;
|
|
1846
|
+
} catch {
|
|
1847
|
+
return void 0;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function b64url(bytes) {
|
|
1851
|
+
let s = "";
|
|
1852
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
1853
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1854
|
+
}
|
|
1855
|
+
function random(bytes = 32) {
|
|
1856
|
+
return b64url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
1857
|
+
}
|
|
1858
|
+
function localPath(candidate, fallback) {
|
|
1859
|
+
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//") || candidate.includes("\\")) return fallback;
|
|
1860
|
+
if (/[\u0000-\u001f\u007f]/.test(candidate)) return fallback;
|
|
1861
|
+
try {
|
|
1862
|
+
if (new URL(candidate, "http://x").origin !== "http://x") return fallback;
|
|
1863
|
+
} catch {
|
|
1864
|
+
return fallback;
|
|
1865
|
+
}
|
|
1866
|
+
return candidate;
|
|
1867
|
+
}
|
|
1868
|
+
function json(body, status = 200, headers) {
|
|
1869
|
+
const h = headers ?? new Headers();
|
|
1870
|
+
h.set("content-type", "application/json; charset=utf-8");
|
|
1871
|
+
h.set("cache-control", "no-store");
|
|
1872
|
+
return new Response(JSON.stringify(body), { status, headers: h });
|
|
1873
|
+
}
|
|
1874
|
+
function redirect(location, cookies = []) {
|
|
1875
|
+
const h = new Headers({ location, "cache-control": "no-store" });
|
|
1876
|
+
for (const c of cookies) h.append("set-cookie", c);
|
|
1877
|
+
return new Response(null, { status: 302, headers: h });
|
|
1878
|
+
}
|
|
1879
|
+
function conditionalOf(c) {
|
|
1880
|
+
return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
|
|
1881
|
+
}
|
|
1882
|
+
function byContract(given) {
|
|
1883
|
+
failClosed(typeof process !== "undefined" ? process.env : void 0);
|
|
1884
|
+
for (const [name, value] of [
|
|
1885
|
+
["clientId", given.clientId],
|
|
1886
|
+
["clientSecret", given.clientSecret],
|
|
1887
|
+
["secret", given.secret],
|
|
1888
|
+
["system", given.system],
|
|
1889
|
+
["tenant", given.tenant],
|
|
1890
|
+
["systemKey", given.systemKey]
|
|
1891
|
+
]) {
|
|
1892
|
+
if (value !== void 0) throw new CauthyConfigError(`with a contract, ${name} comes from the contract: do not pass it`);
|
|
1893
|
+
}
|
|
1894
|
+
const contract = readContract(given.contract);
|
|
1895
|
+
const root = given.rootKey ?? env3("PACT_ROOT_KEY") ?? "";
|
|
1896
|
+
if (root === "") throw new CauthyConfigError("rootKey (PACT_ROOT_KEY) is required with a contract: 32 random bytes, e.g. openssl rand -base64 32");
|
|
1897
|
+
if (contract.serviceClientId && !given.keys) {
|
|
1898
|
+
throw new CauthyConfigError(`the link ${contract.link} gives ${contract.system} its own identity: pass keys, where its key is kept (@cauthyhq/node/keys)`);
|
|
1899
|
+
}
|
|
1900
|
+
return { contract, root, previous: given.rootKeyPrevious ?? env3("PACT_ROOT_KEY_PREVIOUS") };
|
|
1901
|
+
}
|
|
1902
|
+
var toldOfHeader = false;
|
|
1903
|
+
function createCauthy(given = {}) {
|
|
1904
|
+
const pacted = given.contract ? byContract(given) : void 0;
|
|
1905
|
+
const contract = pacted?.contract;
|
|
1906
|
+
const issuer = withoutTrailingSlashes(given.issuer ?? contract?.issuer ?? env3("CAUTHY_ISSUER") ?? "");
|
|
1907
|
+
const clientId = contract?.clientId ?? given.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
|
|
1908
|
+
const clientSecret = contract ? void 0 : given.clientSecret ?? env3("CAUTHY_CLIENT_SECRET");
|
|
1909
|
+
const secret = contract ? "" : given.secret ?? env3("CAUTHY_SECRET") ?? "";
|
|
1910
|
+
const system = contract?.system ?? given.system ?? env3("CAUTHY_SYSTEM") ?? "";
|
|
1911
|
+
const tenant = contract?.tenant ?? given.tenant ?? env3("CAUTHY_TENANT") ?? "";
|
|
1912
|
+
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
|
|
1913
|
+
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
1914
|
+
if (!contract && secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
|
|
1915
|
+
const derived = pacted ? deriveKeys(pacted.root, pacted.previous) : void 0;
|
|
1916
|
+
const single = derived ? void 0 : keyOf(secret).then((k) => [k]);
|
|
1917
|
+
const cookieKeys = derived?.cookie ?? single;
|
|
1918
|
+
const flowKeys = derived?.flow ?? single;
|
|
1919
|
+
const ownKeys = derived && given.keys ? pactKeys(given.keys, derived.seal) : void 0;
|
|
1920
|
+
const baseURL = given.baseURL ?? env3("CAUTHY_BASE_URL");
|
|
1921
|
+
if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
|
|
1922
|
+
throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
|
|
1923
|
+
}
|
|
1924
|
+
const basePath = "/" + (given.basePath ?? "/api/auth").replace(/^\/+|\/+$/g, "");
|
|
1925
|
+
const requestHeader = given.requestHeader ?? "required";
|
|
1926
|
+
if (requestHeader !== "required" && requestHeader !== "warn") throw new CauthyConfigError("requestHeader must be 'required' or 'warn'");
|
|
1927
|
+
const afterSignIn = localPath(given.afterSignIn, "/");
|
|
1928
|
+
const afterSignOut = localPath(given.afterSignOut, "/");
|
|
1929
|
+
const cookieName = given.cookieName ?? "cauthy_session";
|
|
1930
|
+
const flowCookie = `${cookieName}_flow`;
|
|
1931
|
+
const againCookie = `${cookieName}_again`;
|
|
1932
|
+
const stepCookie = `${cookieName}_step`;
|
|
1933
|
+
const systemTimeout = given.timeouts?.system ?? 2e3;
|
|
1934
|
+
const requestTimeout = given.timeouts?.request ?? 5e3;
|
|
1935
|
+
for (const [name, ms] of [["system", systemTimeout], ["request", requestTimeout]]) {
|
|
1936
|
+
if (!Number.isFinite(ms) || ms <= 0) throw new CauthyConfigError(`timeouts.${name} must be a positive number of milliseconds`);
|
|
1937
|
+
}
|
|
1938
|
+
const doFetch = deadline(given.fetch, requestTimeout);
|
|
1939
|
+
const iconOf = iconFor(issuer);
|
|
1940
|
+
const pages = {
|
|
1941
|
+
signIn: given.pages?.signIn ? localPath(given.pages.signIn, "/") : void 0,
|
|
1942
|
+
resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
|
|
1943
|
+
verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0,
|
|
1944
|
+
acceptInvite: given.pages?.acceptInvite ? localPath(given.pages.acceptInvite, "/") : void 0
|
|
1945
|
+
};
|
|
1946
|
+
const acceptInviteUrl = baseURL && pages.acceptInvite ? `${withoutTrailingSlashes(baseURL)}${pages.acceptInvite}` : void 0;
|
|
1947
|
+
const ownSystem = createSystem({
|
|
1948
|
+
issuer,
|
|
1949
|
+
clientId,
|
|
1950
|
+
clientSecret,
|
|
1951
|
+
key: given.systemKey,
|
|
1952
|
+
system: system || void 0,
|
|
1953
|
+
acceptInviteUrl,
|
|
1954
|
+
fetch: given.fetch,
|
|
1955
|
+
timeoutMs: systemTimeout,
|
|
1956
|
+
...ownKeys ? { keys: ownKeys, keysClientId: contract?.serviceClientId } : {}
|
|
1957
|
+
});
|
|
1958
|
+
const originOf = (request) => {
|
|
1959
|
+
if (baseURL) return withoutTrailingSlashes(baseURL);
|
|
1960
|
+
const url = new URL(request.url);
|
|
1961
|
+
const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
|
|
1962
|
+
return proto === "https" || proto === "http" ? `${proto}://${url.host}` : url.origin;
|
|
1963
|
+
};
|
|
1964
|
+
const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
|
|
1965
|
+
async function token(form, proof = {}) {
|
|
1966
|
+
const body2 = new URLSearchParams({ client_id: clientId, ...form });
|
|
1967
|
+
if (clientSecret) body2.set("client_secret", clientSecret);
|
|
1968
|
+
try {
|
|
1969
|
+
const res = await doFetch(`${issuer}/v1/oauth/token`, {
|
|
1970
|
+
method: "POST",
|
|
1971
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json", ...proof },
|
|
1972
|
+
body: body2
|
|
1973
|
+
});
|
|
1974
|
+
if (res.ok) return { ok: true, body: await res.json() };
|
|
1975
|
+
const why = await res.json().catch(() => null);
|
|
1976
|
+
return { ok: false, ended: res.status >= 400 && res.status < 500, ...typeof why?.error === "string" ? { code: why.error } : {} };
|
|
1977
|
+
} catch {
|
|
1978
|
+
return { ok: false, ended: false };
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
async function userinfo(accessToken) {
|
|
1982
|
+
try {
|
|
1983
|
+
const res = await doFetch(`${issuer}/v1/oauth/userinfo`, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
1984
|
+
if (!res.ok) return null;
|
|
1985
|
+
const b = await res.json();
|
|
1986
|
+
return {
|
|
1987
|
+
tid: String(b.tid ?? ""),
|
|
1988
|
+
roles: Array.isArray(b.roles) ? b.roles.filter((r) => typeof r === "string") : [],
|
|
1989
|
+
u: {
|
|
1990
|
+
id: String(b.sub ?? ""),
|
|
1991
|
+
email: String(b.email ?? ""),
|
|
1992
|
+
name: String(b.name ?? ""),
|
|
1993
|
+
emailVerified: b.email_verified === true,
|
|
1994
|
+
restricted: b.restricted === true,
|
|
1995
|
+
...typeof b.email_verify_by === "string" ? { emailVerifyBy: b.email_verify_by } : {},
|
|
1996
|
+
...typeof b.locale === "string" && b.locale !== "" ? { locale: b.locale } : {},
|
|
1997
|
+
...b.anonymous === true ? { isAnonymous: true } : {}
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
} catch {
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
const kindOf = (body2, was) => (typeof body2?.persistent === "boolean" ? body2.persistent : was?.p === 1) ? { p: 1 } : {};
|
|
2005
|
+
async function revoke(refreshToken) {
|
|
2006
|
+
const form = new URLSearchParams({ client_id: clientId, token: refreshToken });
|
|
2007
|
+
if (clientSecret) form.set("client_secret", clientSecret);
|
|
2008
|
+
await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
|
|
2009
|
+
}
|
|
2010
|
+
const unverified = (u) => contract?.requireVerifiedEmail === true && !u.emailVerified;
|
|
2011
|
+
async function stored(body2, was) {
|
|
2012
|
+
if (typeof body2?.access_token !== "string" || typeof body2?.refresh_token !== "string") return null;
|
|
2013
|
+
const who = await userinfo(body2.access_token);
|
|
2014
|
+
if (who === null || who.u.id === "") return null;
|
|
2015
|
+
if (unverified(who.u)) {
|
|
2016
|
+
await revoke(body2.refresh_token);
|
|
2017
|
+
throw new CauthyApiError(403, "email_not_confirmed", "Confirm your email address first.");
|
|
2018
|
+
}
|
|
2019
|
+
const lifetime = typeof body2.expires_in === "number" && body2.expires_in > 0 ? body2.expires_in : 300;
|
|
2020
|
+
return { at: body2.access_token, rt: body2.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who, ...kindOf(body2, was) };
|
|
2021
|
+
}
|
|
2022
|
+
function toSession(s) {
|
|
2023
|
+
return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1e3).toISOString() };
|
|
2024
|
+
}
|
|
2025
|
+
async function read(request) {
|
|
2026
|
+
const had = readCookies(request.headers.get("cookie"));
|
|
2027
|
+
const raw = readParts(had, cookieName);
|
|
2028
|
+
if (raw === null) return { had, value: null, present: false };
|
|
2029
|
+
const value = await unsealAny(raw, await cookieKeys);
|
|
2030
|
+
const sound = value !== null && typeof value.at === "string" && typeof value.rt === "string" && typeof value.exp === "number";
|
|
2031
|
+
return { had, value: sound ? value : null, present: true };
|
|
2032
|
+
}
|
|
2033
|
+
async function write(request, s, had) {
|
|
2034
|
+
return writeParts(cookieName, await seal(s, (await cookieKeys)[0]), attrs(request, s.p === 1 ? PERSISTENT_SECONDS : void 0), had);
|
|
2035
|
+
}
|
|
2036
|
+
const fresh = (s) => s.exp - Date.now() / 1e3 > SKEW_SECONDS;
|
|
2037
|
+
const renewing = /* @__PURE__ */ new Map();
|
|
2038
|
+
function renew(was) {
|
|
2039
|
+
let running = renewing.get(was.rt);
|
|
2040
|
+
if (running === void 0) {
|
|
2041
|
+
const rt = was.rt;
|
|
2042
|
+
running = (async () => {
|
|
2043
|
+
const res = await token({ grant_type: "refresh_token", refresh_token: rt });
|
|
2044
|
+
if (!res.ok) return res.ended ? "ended" : "unreachable";
|
|
2045
|
+
let next;
|
|
2046
|
+
try {
|
|
2047
|
+
next = await stored(res.body, was);
|
|
2048
|
+
} catch (err) {
|
|
2049
|
+
if (err instanceof CauthyApiError && err.code === "email_not_confirmed") return "ended";
|
|
2050
|
+
throw err;
|
|
2051
|
+
}
|
|
2052
|
+
if (next !== null) return next;
|
|
2053
|
+
if (typeof res.body?.access_token !== "string" || typeof res.body?.refresh_token !== "string") return "unreachable";
|
|
2054
|
+
const lifetime = typeof res.body.expires_in === "number" && res.body.expires_in > 0 ? res.body.expires_in : 300;
|
|
2055
|
+
const { p: _, ...rest } = was;
|
|
2056
|
+
return { ...rest, at: res.body.access_token, rt: res.body.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...kindOf(res.body, was) };
|
|
2057
|
+
})();
|
|
2058
|
+
renewing.set(rt, running);
|
|
2059
|
+
void running.then((result) => {
|
|
2060
|
+
if (result === "unreachable") return renewing.delete(rt);
|
|
2061
|
+
const timer = setTimeout(() => renewing.delete(rt), 5e3);
|
|
2062
|
+
timer.unref?.();
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
return running;
|
|
2066
|
+
}
|
|
2067
|
+
async function resolve2(request) {
|
|
2068
|
+
const { had, value, present } = await read(request);
|
|
2069
|
+
if (value === null) {
|
|
2070
|
+
return { session: null, status: "unauthenticated", cookies: present ? clearParts(cookieName, attrs(request, 0), had) : [] };
|
|
2071
|
+
}
|
|
2072
|
+
if (unverified(value.u)) return { session: null, status: "unauthenticated", cookies: clearParts(cookieName, attrs(request, 0), had) };
|
|
2073
|
+
if (fresh(value)) return { session: toSession(value), status: "authenticated", cookies: [] };
|
|
2074
|
+
const renewed = await renew(value);
|
|
2075
|
+
if (renewed === "ended") {
|
|
2076
|
+
return { session: null, status: "unauthenticated", cookies: clearParts(cookieName, attrs(request, 0), had) };
|
|
2077
|
+
}
|
|
2078
|
+
if (renewed === "unreachable") return { session: null, status: "expired", cookies: [] };
|
|
2079
|
+
return { session: toSession(renewed), status: "authenticated", cookies: await write(request, renewed, had) };
|
|
2080
|
+
}
|
|
2081
|
+
async function getSession(request, responseHeaders) {
|
|
2082
|
+
if (responseHeaders === void 0) {
|
|
2083
|
+
const { value } = await read(request);
|
|
2084
|
+
return value !== null && fresh(value) && !unverified(value.u) ? toSession(value) : null;
|
|
2085
|
+
}
|
|
2086
|
+
const r = await resolve2(request);
|
|
2087
|
+
for (const c of r.cookies) responseHeaders.append("set-cookie", c);
|
|
2088
|
+
return r.session;
|
|
2089
|
+
}
|
|
2090
|
+
async function status(request) {
|
|
2091
|
+
const { value } = await read(request);
|
|
2092
|
+
if (value === null || unverified(value.u)) return "unauthenticated";
|
|
2093
|
+
return fresh(value) ? "authenticated" : "expired";
|
|
2094
|
+
}
|
|
2095
|
+
const orgCache = /* @__PURE__ */ new Map();
|
|
2096
|
+
async function organizations(session) {
|
|
2097
|
+
const hit = orgCache.get(session.accessToken);
|
|
2098
|
+
if (hit && Date.now() - hit.at < ORGANIZATIONS_TTL_MS) return hit.value;
|
|
2099
|
+
const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
|
|
2100
|
+
const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
|
|
2101
|
+
if (!res.ok) throw new Error(`@cauthyhq/node: reading organizations: ${res.status}`);
|
|
2102
|
+
const body2 = await res.json();
|
|
2103
|
+
const value = (body2.data ?? []).map(
|
|
2104
|
+
(o) => ({
|
|
2105
|
+
id: String(o.id),
|
|
2106
|
+
system: String(o.system ?? ""),
|
|
2107
|
+
name: String(o.name ?? ""),
|
|
2108
|
+
kind: String(o.kind ?? ""),
|
|
2109
|
+
role: String(o.role ?? ""),
|
|
2110
|
+
metadata: o.metadata ?? {},
|
|
2111
|
+
...o.external_id ? { externalId: String(o.external_id) } : {},
|
|
2112
|
+
...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [], conditional: Array.isArray(o.conditional) ? o.conditional.map(conditionalOf) : [] } : {}
|
|
2113
|
+
})
|
|
2114
|
+
);
|
|
2115
|
+
if (orgCache.size > 5e3) orgCache.clear();
|
|
2116
|
+
orgCache.set(session.accessToken, { at: Date.now(), value });
|
|
2117
|
+
return value;
|
|
2118
|
+
}
|
|
2119
|
+
async function decide(token2, question) {
|
|
2120
|
+
const org = organizationIdOf(question.organization);
|
|
2121
|
+
if (org === "") return { allowed: false, reason: "no organization named" };
|
|
2122
|
+
const res = await doFetch(`${issuer}/v1/oauth/organizations/${encodeURIComponent(org)}/check`, {
|
|
2123
|
+
method: "POST",
|
|
2124
|
+
headers: { authorization: `Bearer ${token2}`, "content-type": "application/json", accept: "application/json" },
|
|
2125
|
+
body: JSON.stringify(checkWire(question))
|
|
2126
|
+
}).catch(() => null);
|
|
2127
|
+
if (res === null) throw new CauthyApiError(503, "unavailable", "Cauthy could not be reached to check a permission");
|
|
2128
|
+
const body2 = await res.json().catch(() => null);
|
|
2129
|
+
if (res.ok) return checkResultOf(body2?.data);
|
|
2130
|
+
const first = body2?.errors?.[0];
|
|
2131
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
2132
|
+
return { allowed: false, reason: first?.message ?? (res.status === 401 ? "not signed in" : res.status === 404 ? "not a member of the organization" : "refused") };
|
|
2133
|
+
}
|
|
2134
|
+
throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused to check a permission (${res.status}): ${first?.message ?? res.statusText}`);
|
|
2135
|
+
}
|
|
2136
|
+
async function check(who, question) {
|
|
2137
|
+
const session = "accessToken" in who ? who : await getSession(who);
|
|
2138
|
+
if (!session) return { allowed: false, reason: "not signed in" };
|
|
2139
|
+
return decide(session.accessToken, question);
|
|
2140
|
+
}
|
|
2141
|
+
const forgetting = (fn) => async (...args) => {
|
|
2142
|
+
const out2 = await fn(...args);
|
|
2143
|
+
orgCache.clear();
|
|
2144
|
+
return out2;
|
|
2145
|
+
};
|
|
2146
|
+
const systemThatRefreshes = {
|
|
2147
|
+
...ownSystem,
|
|
2148
|
+
organizations: {
|
|
2149
|
+
...ownSystem.organizations,
|
|
2150
|
+
update: forgetting(ownSystem.organizations.update),
|
|
2151
|
+
remove: forgetting(ownSystem.organizations.remove)
|
|
2152
|
+
},
|
|
2153
|
+
members: {
|
|
2154
|
+
...ownSystem.members,
|
|
2155
|
+
add: forgetting(ownSystem.members.add),
|
|
2156
|
+
setRole: forgetting(ownSystem.members.setRole),
|
|
2157
|
+
remove: forgetting(ownSystem.members.remove)
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
async function toPublic(session) {
|
|
2161
|
+
let orgs = [];
|
|
2162
|
+
try {
|
|
2163
|
+
orgs = await organizations(session);
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
return { user: session.user, tenantId: session.tenantId, roles: session.roles, organizations: orgs, expiresAt: session.expiresAt };
|
|
2167
|
+
}
|
|
2168
|
+
const urls = {
|
|
2169
|
+
signIn: (returnTo) => `${basePath}/login${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ""}`,
|
|
2170
|
+
signOut: () => `${basePath}/logout`,
|
|
2171
|
+
account: (returnTo) => `${basePath}/account${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ""}`,
|
|
2172
|
+
session: `${basePath}/session`
|
|
2173
|
+
};
|
|
2174
|
+
async function login(request, url) {
|
|
2175
|
+
const flow = { state: random(), verifier: random(48), returnTo: localPath(url.searchParams.get("return_to"), afterSignIn) };
|
|
2176
|
+
const challenge = b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(flow.verifier))));
|
|
2177
|
+
const q = new URLSearchParams({
|
|
2178
|
+
client_id: clientId,
|
|
2179
|
+
redirect_uri: `${originOf(request)}${basePath}/callback`,
|
|
2180
|
+
response_type: "code",
|
|
2181
|
+
state: flow.state,
|
|
2182
|
+
code_challenge: challenge,
|
|
2183
|
+
code_challenge_method: "S256"
|
|
2184
|
+
});
|
|
2185
|
+
const prompt = url.searchParams.get("prompt");
|
|
2186
|
+
if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
|
|
2187
|
+
const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
|
|
2188
|
+
if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
|
|
2189
|
+
let passkey = "";
|
|
2190
|
+
if (url.searchParams.get("method") === "passkey") {
|
|
2191
|
+
q.set("method", "passkey");
|
|
2192
|
+
passkey = "#passkey";
|
|
2193
|
+
}
|
|
2194
|
+
return redirect(`${issuer}/v1/oauth/authorize?${q}${passkey}`, [
|
|
2195
|
+
writeCookie(flowCookie, await seal(flow, (await flowKeys)[0]), attrs(request, FLOW_SECONDS))
|
|
2196
|
+
]);
|
|
2197
|
+
}
|
|
2198
|
+
function failed(request, reason) {
|
|
2199
|
+
const h = new Headers({ "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
2200
|
+
h.append("set-cookie", writeCookie(flowCookie, "", attrs(request, 0)));
|
|
2201
|
+
h.append("set-cookie", writeCookie(againCookie, "", attrs(request, 0)));
|
|
2202
|
+
return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h });
|
|
2203
|
+
}
|
|
2204
|
+
async function callback(request, url) {
|
|
2205
|
+
const had = readCookies(request.headers.get("cookie"));
|
|
2206
|
+
const sealed = had.get(flowCookie);
|
|
2207
|
+
const flow = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2208
|
+
if (flow === null || url.searchParams.get("state") !== flow.state) {
|
|
2209
|
+
if (had.get(againCookie) === void 0) {
|
|
2210
|
+
const back = flow?.returnTo ?? afterSignIn;
|
|
2211
|
+
return redirect(`${basePath}/login?return_to=${encodeURIComponent(back)}`, [writeCookie(againCookie, "1", attrs(request, AGAIN_SECONDS))]);
|
|
2212
|
+
}
|
|
2213
|
+
return failed(request, "state_mismatch");
|
|
2214
|
+
}
|
|
2215
|
+
const error = url.searchParams.get("error");
|
|
2216
|
+
if (error !== null) {
|
|
2217
|
+
if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
2218
|
+
if (error === "access_denied") {
|
|
2219
|
+
const back = pages.signIn ?? flow.returnTo;
|
|
2220
|
+
return redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=cancelled`, [writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
2221
|
+
}
|
|
2222
|
+
return failed(request, error.slice(0, 60));
|
|
2223
|
+
}
|
|
2224
|
+
const code = url.searchParams.get("code");
|
|
2225
|
+
if (!code) return failed(request, "no_code");
|
|
2226
|
+
const res = await token({
|
|
2227
|
+
grant_type: "authorization_code",
|
|
2228
|
+
code,
|
|
2229
|
+
redirect_uri: `${originOf(request)}${basePath}/callback`,
|
|
2230
|
+
code_verifier: flow.verifier
|
|
2231
|
+
}, await passedOn(request));
|
|
2232
|
+
const unconfirmed = () => {
|
|
2233
|
+
const page = pages.signIn ?? "/";
|
|
2234
|
+
return redirect(
|
|
2235
|
+
`${page}${page.includes("?") ? "&" : "?"}auth_error=email_not_confirmed`,
|
|
2236
|
+
[writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]
|
|
2237
|
+
);
|
|
2238
|
+
};
|
|
2239
|
+
if (!res.ok && res.code === "email_not_confirmed") return unconfirmed();
|
|
2240
|
+
let s;
|
|
2241
|
+
try {
|
|
2242
|
+
s = res.ok ? await stored(res.body) : null;
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
if (!(err instanceof CauthyApiError && err.code === "email_not_confirmed")) throw err;
|
|
2245
|
+
return unconfirmed();
|
|
2246
|
+
}
|
|
2247
|
+
if (s === null) return failed(request, "exchange_failed");
|
|
2248
|
+
return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]);
|
|
2249
|
+
}
|
|
2250
|
+
async function logout(request) {
|
|
2251
|
+
const { had, value } = await read(request);
|
|
2252
|
+
if (value !== null) {
|
|
2253
|
+
const body2 = new URLSearchParams({ client_id: clientId, token: value.rt });
|
|
2254
|
+
if (clientSecret) body2.set("client_secret", clientSecret);
|
|
2255
|
+
await doFetch(`${issuer}/v1/oauth/revoke`, {
|
|
2256
|
+
method: "POST",
|
|
2257
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
2258
|
+
body: body2
|
|
2259
|
+
}).catch(() => void 0);
|
|
2260
|
+
orgCache.delete(value.at);
|
|
2261
|
+
}
|
|
2262
|
+
const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
|
|
2263
|
+
return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
|
|
2264
|
+
}
|
|
2265
|
+
const STEP_SECONDS = 600;
|
|
2266
|
+
const PASSKEY_SECONDS = 300;
|
|
2267
|
+
const factorsOf = (d) => ({
|
|
2268
|
+
totp: d?.totp === true,
|
|
2269
|
+
phone: d?.phone ? { number: String(d.phone.number ?? ""), channel: String(d.phone.channel ?? "sms") } : null,
|
|
2270
|
+
phoneChannels: Array.isArray(d?.phone_channels) ? d.phone_channels.map(String) : []
|
|
2271
|
+
});
|
|
2272
|
+
const isCredential = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.id === "string" && typeof v.response === "object";
|
|
2273
|
+
const passkeyOf = (d) => ({
|
|
2274
|
+
id: String(d?.id ?? ""),
|
|
2275
|
+
name: String(d?.nickname ?? ""),
|
|
2276
|
+
kind: ["security_key", "synced", "device"].includes(d?.kind) ? d.kind : "unknown",
|
|
2277
|
+
createdAt: String(d?.created_at ?? ""),
|
|
2278
|
+
lastUsedAt: d?.last_used_at ? String(d.last_used_at) : null
|
|
2279
|
+
});
|
|
2280
|
+
const UUID3 = /^[0-9a-f-]{36}$/i;
|
|
2281
|
+
const seatOf = (d) => ({
|
|
2282
|
+
id: String(d?.id ?? ""),
|
|
2283
|
+
system: String(d?.system ?? ""),
|
|
2284
|
+
name: String(d?.name ?? ""),
|
|
2285
|
+
kind: String(d?.kind ?? ""),
|
|
2286
|
+
role: String(d?.role ?? ""),
|
|
2287
|
+
// A Cauthy from before a power says nothing of it: then it is not given.
|
|
2288
|
+
can: {
|
|
2289
|
+
readMembers: d?.can?.read_members === true,
|
|
2290
|
+
manageMembers: d?.can?.manage_members === true,
|
|
2291
|
+
removeMembers: d?.can?.remove_members === true,
|
|
2292
|
+
update: d?.can?.update === true,
|
|
2293
|
+
archive: d?.can?.archive === true,
|
|
2294
|
+
delete: d?.can?.delete === true
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
const orgMemberOf = (d) => ({
|
|
2298
|
+
userId: String(d?.user_id ?? ""),
|
|
2299
|
+
email: String(d?.email ?? ""),
|
|
2300
|
+
name: String(d?.name ?? ""),
|
|
2301
|
+
role: String(d?.role ?? ""),
|
|
2302
|
+
status: String(d?.status ?? ""),
|
|
2303
|
+
invited: d?.invited === true,
|
|
2304
|
+
manager: d?.manager === true,
|
|
2305
|
+
you: d?.you === true,
|
|
2306
|
+
since: String(d?.since ?? "")
|
|
2307
|
+
});
|
|
2308
|
+
const orgRoleOf = (d) => ({
|
|
2309
|
+
role: String(d?.role ?? ""),
|
|
2310
|
+
name: String(d?.name ?? ""),
|
|
2311
|
+
description: String(d?.description ?? ""),
|
|
2312
|
+
manager: d?.manager === true
|
|
2313
|
+
});
|
|
2314
|
+
const agentOf = (d) => ({
|
|
2315
|
+
id: String(d?.id ?? ""),
|
|
2316
|
+
name: String(d?.agent?.name ?? ""),
|
|
2317
|
+
redirectHost: String(d?.agent?.redirect_host ?? ""),
|
|
2318
|
+
resource: { name: String(d?.resource?.name ?? ""), uri: String(d?.resource?.uri ?? "") },
|
|
2319
|
+
scopes: Array.isArray(d?.scopes) ? d.scopes.map((s) => ({ name: String(s?.name ?? ""), description: String(s?.description ?? "") })) : [],
|
|
2320
|
+
connectedAt: String(d?.created_at ?? ""),
|
|
2321
|
+
lastUsedAt: String(d?.last_used_at ?? "")
|
|
2322
|
+
});
|
|
2323
|
+
const wants = (v) => v === true || v === "1" || v === "on" || v === "true";
|
|
2324
|
+
const problem = (status2, code, message, field, headers, reasons) => json({ error: { code, message, ...field ? { field } : {}, ...reasons && reasons.length > 0 ? { reasons } : {} } }, status2, headers);
|
|
2325
|
+
const invalid = (p) => problem(400, p.code, p.message, p.field);
|
|
2326
|
+
let explained = false;
|
|
2327
|
+
function withoutRequestHeader(request, route) {
|
|
2328
|
+
if (request.headers.get("x-cauthy-request") === "1") return null;
|
|
2329
|
+
if (requestHeader === "warn") {
|
|
2330
|
+
if (!toldOfHeader) {
|
|
2331
|
+
toldOfHeader = true;
|
|
2332
|
+
console.warn(`@cauthyhq/node: POST ${basePath}${route} arrived without X-Cauthy-Request: 1 and was let through (requestHeader: 'warn'). Update the browser to cauthy-auth 0.43 or @cauthyhq/js, then remove requestHeader.`);
|
|
2333
|
+
}
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
return problem(403, "csrf_required", "This request did not come from this application's own client.");
|
|
2337
|
+
}
|
|
2338
|
+
function ownRequest(request) {
|
|
2339
|
+
const origin = request.headers.get("origin");
|
|
2340
|
+
if (origin !== null) {
|
|
2341
|
+
const own = originOf(request);
|
|
2342
|
+
if (origin === own || origin === new URL(request.url).origin) return true;
|
|
2343
|
+
if (!explained && hostOf(origin) === new URL(own).hostname) {
|
|
2344
|
+
explained = true;
|
|
2345
|
+
console.warn(`@cauthyhq/node: refused a request from ${origin} as cross_site: this application sees itself as ${own}. Behind a proxy? Set CAUTHY_BASE_URL (or baseURL) to the public origin.`);
|
|
2346
|
+
}
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
|
|
2350
|
+
}
|
|
2351
|
+
async function body(request) {
|
|
2352
|
+
if (!(request.headers.get("content-type") ?? "").includes("application/json")) return null;
|
|
2353
|
+
try {
|
|
2354
|
+
const b = await request.json();
|
|
2355
|
+
return typeof b === "object" && b !== null && !Array.isArray(b) ? b : null;
|
|
2356
|
+
} catch {
|
|
2357
|
+
return null;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const clientIp = given.clientIp ?? ((request) => request.headers.get("x-forwarded-for")?.split(",").map((a) => a.trim()).reverse().find((a) => a !== "" && !ownNetwork(a)));
|
|
2361
|
+
let noProofUntil = 0;
|
|
2362
|
+
async function passedOn(request) {
|
|
2363
|
+
if (Date.now() < noProofUntil) return {};
|
|
2364
|
+
const ip = clientIp(request);
|
|
2365
|
+
try {
|
|
2366
|
+
const proof = { "x-cauthy-system-token": await ownSystem.token(), "x-cauthy-app-origin": originOf(request) };
|
|
2367
|
+
return ip ? { ...proof, "x-cauthy-client-ip": ip } : proof;
|
|
2368
|
+
} catch {
|
|
2369
|
+
noProofUntil = Date.now() + 3e4;
|
|
2370
|
+
return {};
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
async function ask(request, method, path, payload, page, bearer2, remember = false) {
|
|
2374
|
+
try {
|
|
2375
|
+
const res = await doFetch(`${issuer}${path}`, {
|
|
2376
|
+
method,
|
|
2377
|
+
headers: {
|
|
2378
|
+
"x-tenant-key": tenant,
|
|
2379
|
+
accept: "application/json",
|
|
2380
|
+
// A link Cauthy mails opens the application's page for it when it
|
|
2381
|
+
// has one; otherwise Cauthy's own. Either way in the reader's language.
|
|
2382
|
+
...page ? { "x-cauthy-link-to": `${originOf(request)}${page}` } : { "x-cauthy-links": "hosted" },
|
|
2383
|
+
// Whose request it is: what Cauthy mails goes in this application's
|
|
2384
|
+
// name, under its logo.
|
|
2385
|
+
...clientId ? { "x-cauthy-client-id": clientId } : {},
|
|
2386
|
+
...await passedOn(request),
|
|
2387
|
+
...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
|
|
2388
|
+
// The person asked to stay signed in: Cauthy keeps the session long if the client allows it.
|
|
2389
|
+
...remember ? { "x-cauthy-remember": "1" } : {},
|
|
2390
|
+
// The answer to the workspace's Turnstile challenge, from the
|
|
2391
|
+
// person's browser: Cauthy checks it at the doors that ask for it.
|
|
2392
|
+
...request.headers.get("x-cauthy-captcha") ? { "x-captcha-token": request.headers.get("x-cauthy-captcha") } : {},
|
|
2393
|
+
...request.headers.get("accept-language") ? { "accept-language": request.headers.get("accept-language") } : {},
|
|
2394
|
+
// The language the application says its person reads, over the browser's.
|
|
2395
|
+
...request.headers.get("x-cauthy-locale") ? { "x-cauthy-locale": request.headers.get("x-cauthy-locale") } : {},
|
|
2396
|
+
...request.headers.get("user-agent") ? { "user-agent": request.headers.get("user-agent") } : {},
|
|
2397
|
+
...payload === void 0 ? {} : { "content-type": "application/json" }
|
|
2398
|
+
},
|
|
2399
|
+
body: payload === void 0 ? void 0 : JSON.stringify(payload)
|
|
2400
|
+
});
|
|
2401
|
+
const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
|
|
2402
|
+
if (res.ok) return { status: res.status, data: b?.data ?? b };
|
|
2403
|
+
const first = b?.errors?.[0];
|
|
2404
|
+
const reasons = Array.isArray(first?.meta?.reasons) ? first.meta.reasons.map(String) : void 0;
|
|
2405
|
+
return { status: res.status, data: null, error: { code: String(first?.code ?? "error"), message: String(first?.message ?? "The request could not be completed."), ...reasons ? { reasons } : {} } };
|
|
2406
|
+
} catch {
|
|
2407
|
+
return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
const accountOf = (d) => ({
|
|
2411
|
+
id: String(d?.id ?? ""),
|
|
2412
|
+
email: String(d?.email ?? ""),
|
|
2413
|
+
name: String(d?.name ?? ""),
|
|
2414
|
+
emailVerified: d?.email_verified === true,
|
|
2415
|
+
hasPassword: d?.has_password === true,
|
|
2416
|
+
mfaEnabled: d?.mfa_enabled === true
|
|
2417
|
+
});
|
|
2418
|
+
async function signedInAs(request) {
|
|
2419
|
+
const r = await resolve2(request);
|
|
2420
|
+
const headers = new Headers();
|
|
2421
|
+
for (const c of r.cookies) headers.append("set-cookie", c);
|
|
2422
|
+
if (r.session === null) return { response: problem(401, "unauthenticated", "Sign in first.", void 0, headers) };
|
|
2423
|
+
return { token: r.session.accessToken, kept: r.cookies.length === 0 ? (await read(request)).value : null, headers };
|
|
2424
|
+
}
|
|
2425
|
+
async function signedIn(request, tokens, extra = [], more = {}, was) {
|
|
2426
|
+
const s = await stored(tokens, was);
|
|
2427
|
+
if (s === null) return problem(502, "unavailable", "Sign-in is unavailable right now. Try again in a moment.");
|
|
2428
|
+
const h = new Headers();
|
|
2429
|
+
for (const c of [...await write(request, s, readCookies(request.headers.get("cookie"))), ...extra]) h.append("set-cookie", c);
|
|
2430
|
+
return json({ status: "signed_in", session: await toPublic(toSession(s)), ...more }, 200, h);
|
|
2431
|
+
}
|
|
2432
|
+
async function loggedIn(request, data, remember = false) {
|
|
2433
|
+
if (data?.mfa_required === true) {
|
|
2434
|
+
const step = { kind: "mfa", token: String(data.mfa_token ?? ""), returnTo: afterSignIn, ...remember ? { remember } : {} };
|
|
2435
|
+
const how = data.mfa_method === "email" ? { method: "email" } : {};
|
|
2436
|
+
return json({ status: "mfa_required", ...how }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, STEP_SECONDS)) }));
|
|
2437
|
+
}
|
|
2438
|
+
return signedIn(request, data);
|
|
2439
|
+
}
|
|
2440
|
+
let lookCache = null;
|
|
2441
|
+
let policyCache = null;
|
|
2442
|
+
const keepPolicy = (settings) => {
|
|
2443
|
+
policyCache = { until: Date.now() + LOOK_TTL_MS, policy: settings.error || !settings.data?.password_policy ? null : passwordPolicyOf(settings.data.password_policy) };
|
|
2444
|
+
return policyCache.policy;
|
|
2445
|
+
};
|
|
2446
|
+
async function workspacePolicy(request) {
|
|
2447
|
+
if (policyCache !== null && policyCache.until >= Date.now()) return policyCache.policy;
|
|
2448
|
+
return keepPolicy(await ask(request, "GET", "/v1/auth/settings"));
|
|
2449
|
+
}
|
|
2450
|
+
async function newPasswordProblem(request, value) {
|
|
2451
|
+
const policy = await workspacePolicy(request);
|
|
2452
|
+
return checkNewPassword(value, policy ?? { min: 8, max: 128, requires: [], min_classes: 0, reject_breached: false });
|
|
2453
|
+
}
|
|
2454
|
+
async function ownRoute(request, url, route) {
|
|
2455
|
+
const post = request.method === "POST";
|
|
2456
|
+
const own = [
|
|
2457
|
+
"/sign-in/email",
|
|
2458
|
+
"/sign-in/mfa",
|
|
2459
|
+
"/sign-up/email",
|
|
2460
|
+
"/forgot-password",
|
|
2461
|
+
"/reset-password",
|
|
2462
|
+
"/verify-email",
|
|
2463
|
+
"/sign-in/magic-link",
|
|
2464
|
+
"/sign-in/magic-link/verify",
|
|
2465
|
+
"/sign-in/email-code",
|
|
2466
|
+
"/me",
|
|
2467
|
+
"/me/password",
|
|
2468
|
+
"/me/mfa/setup",
|
|
2469
|
+
"/me/mfa/verify",
|
|
2470
|
+
"/me/mfa/disable",
|
|
2471
|
+
"/me/mfa/recovery-codes",
|
|
2472
|
+
"/me/mfa/factors",
|
|
2473
|
+
"/me/mfa/phone",
|
|
2474
|
+
"/me/mfa/phone/verify",
|
|
2475
|
+
"/me/mfa/phone/remove",
|
|
2476
|
+
"/sign-in/mfa/methods",
|
|
2477
|
+
"/sign-in/mfa/send",
|
|
2478
|
+
"/sign-in/mfa/email",
|
|
2479
|
+
"/sign-in/mfa/push",
|
|
2480
|
+
"/sign-in/mfa/push/events",
|
|
2481
|
+
"/sign-in/mfa/push/finish",
|
|
2482
|
+
"/me/sessions",
|
|
2483
|
+
"/me/sessions/end",
|
|
2484
|
+
"/me/legal",
|
|
2485
|
+
"/me/legal/accept",
|
|
2486
|
+
"/me/email",
|
|
2487
|
+
"/me/identities",
|
|
2488
|
+
"/me/identities/unlink",
|
|
2489
|
+
"/sign-out",
|
|
2490
|
+
"/methods",
|
|
2491
|
+
"/look",
|
|
2492
|
+
"/sign-in/passkey/begin",
|
|
2493
|
+
"/sign-in/passkey/finish",
|
|
2494
|
+
"/sign-in/google-one-tap/begin",
|
|
2495
|
+
"/sign-in/google-one-tap",
|
|
2496
|
+
"/me/passkeys",
|
|
2497
|
+
"/me/passkeys/add/begin",
|
|
2498
|
+
"/me/passkeys/add/finish",
|
|
2499
|
+
"/me/passkeys/remove",
|
|
2500
|
+
"/me/agents",
|
|
2501
|
+
"/me/agents/disconnect",
|
|
2502
|
+
"/sign-in/anonymous",
|
|
2503
|
+
"/me/upgrade",
|
|
2504
|
+
"/me/organizations",
|
|
2505
|
+
"/me/organizations/members",
|
|
2506
|
+
"/me/organizations/roles",
|
|
2507
|
+
"/me/organizations/members/add",
|
|
2508
|
+
"/me/organizations/members/role",
|
|
2509
|
+
"/me/organizations/members/remove",
|
|
2510
|
+
"/me/organizations/members/invite",
|
|
2511
|
+
"/me/organizations/rename",
|
|
2512
|
+
"/me/organizations/archive",
|
|
2513
|
+
"/me/organizations/delete",
|
|
2514
|
+
"/invitation",
|
|
2515
|
+
"/invitation/accept"
|
|
2516
|
+
].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/") || route.startsWith("/me/upgrade/social/") || route === "/sign-in/sso" || route === "/sign-in/sso/discover" || route === "/sso/callback";
|
|
2517
|
+
if (!own) return null;
|
|
2518
|
+
if (post) {
|
|
2519
|
+
const refused = withoutRequestHeader(request, route);
|
|
2520
|
+
if (refused) return refused;
|
|
2521
|
+
}
|
|
2522
|
+
if (route === "/sign-in/anonymous" || route === "/me/upgrade") deprecated(`POST ${route}`, "Guest sessions leave the auth-bff protocol in version 2.");
|
|
2523
|
+
if (route.startsWith("/me/upgrade/social/")) deprecated("GET /me/upgrade/social/{provider}", "Guest sessions leave the auth-bff protocol in version 2.");
|
|
2524
|
+
if (tenant === "") {
|
|
2525
|
+
return problem(500, "not_configured", "@cauthyhq/node: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
|
|
2526
|
+
}
|
|
2527
|
+
if (route === "/look" && !post) {
|
|
2528
|
+
if (lookCache === null || lookCache.until < Date.now()) {
|
|
2529
|
+
const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(clientId)}`);
|
|
2530
|
+
lookCache = { until: Date.now() + LOOK_TTL_MS, look: res.error ? {} : res.data ?? {} };
|
|
2531
|
+
}
|
|
2532
|
+
return json(lookCache.look);
|
|
2533
|
+
}
|
|
2534
|
+
if (route === "/methods" && !post) {
|
|
2535
|
+
const [providers, settings, place, sso] = await Promise.all([
|
|
2536
|
+
ask(request, "GET", "/v1/auth/social/providers"),
|
|
2537
|
+
ask(request, "GET", "/v1/auth/settings"),
|
|
2538
|
+
ask(request, "GET", `/v1/auth/webauthn/place?origin=${encodeURIComponent(originOf(request))}`),
|
|
2539
|
+
ask(request, "GET", "/v1/auth/sso/connections")
|
|
2540
|
+
]);
|
|
2541
|
+
const list = Array.isArray(providers.data) ? providers.data : [];
|
|
2542
|
+
const password = keepPolicy(settings);
|
|
2543
|
+
const passkeys = settings.data?.allow_passkeys !== false;
|
|
2544
|
+
const here = passkeys && place.data?.here === true;
|
|
2545
|
+
return json({
|
|
2546
|
+
providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? ""), ...iconOf(p) })).filter((p) => p.id !== ""),
|
|
2547
|
+
// Whether the workspace signs people in with a password at all. A
|
|
2548
|
+
// Cauthy from before the setting says nothing of it: then it does.
|
|
2549
|
+
passwordSignIn: settings.data?.allow_password !== false,
|
|
2550
|
+
// The workspace's switch, and how the application's own system takes sign-ups.
|
|
2551
|
+
signUp: settings.data?.allow_signup !== false && settings.data?.signup?.mode !== "invite_only",
|
|
2552
|
+
// The application's system makes an organization per sign-up: the form asks the company's name.
|
|
2553
|
+
...settings.data?.signup?.organization?.ask === true ? { organization: { ask: true, required: settings.data.signup.organization.required === true } } : {},
|
|
2554
|
+
// Whether the person may ask to stay signed in: the client's choice in Cauthy.
|
|
2555
|
+
...typeof settings.data?.persistent_sessions === "boolean" ? { persistentSessions: settings.data.persistent_sessions } : {},
|
|
2556
|
+
// A sign-in link needs a page of the application to open.
|
|
2557
|
+
magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
|
|
2558
|
+
passkey: passkeys && (place.error ? true : here || place.data?.on_cauthy === true),
|
|
2559
|
+
passkeyHere: here,
|
|
2560
|
+
passkeyOnCauthy: passkeys && (place.error ? true : place.data?.on_cauthy === true),
|
|
2561
|
+
// A company's sign-in, found by the person's work address.
|
|
2562
|
+
sso: Array.isArray(sso.data) && sso.data.some((c) => c?.discoverable === true),
|
|
2563
|
+
// The terms a sign-up accepts and the consents it answers, when the workspace asks any.
|
|
2564
|
+
...settings.data?.legal ? { legal: legalOf(settings.data.legal) } : {},
|
|
2565
|
+
...settings.data?.captcha_enabled === true && settings.data?.captcha_site_key ? { captchaSiteKey: String(settings.data.captcha_site_key) } : {},
|
|
2566
|
+
// What a new password must be: for the form to say it and check it.
|
|
2567
|
+
...password ? { password } : {}
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
if (route === "/invitation" && !post) {
|
|
2571
|
+
const token2 = (url.searchParams.get("token") ?? "").trim();
|
|
2572
|
+
if (token2 === "" || token2.length > 512) return problem(422, "invalid_token", "This invitation is not valid, or it has expired.");
|
|
2573
|
+
const res = await ask(request, "GET", `/v1/auth/invitation?token=${encodeURIComponent(token2)}`);
|
|
2574
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
2575
|
+
const organization = typeof res.data?.organization === "string" && res.data.organization !== "" ? { organization: res.data.organization } : {};
|
|
2576
|
+
return json({ status: "ok", invitation: { email: String(res.data?.email ?? ""), workspace: String(res.data?.workspace ?? ""), ...organization } });
|
|
2577
|
+
}
|
|
2578
|
+
if (route === "/sign-in/sso" && !post) {
|
|
2579
|
+
const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
2580
|
+
const failed2 = (code) => redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code)}`);
|
|
2581
|
+
const email = (url.searchParams.get("email") ?? "").trim();
|
|
2582
|
+
const connection = (url.searchParams.get("connection") ?? "").trim();
|
|
2583
|
+
if (email === "" && connection === "") return failed2("sso_not_found");
|
|
2584
|
+
const verifier = random(48);
|
|
2585
|
+
const challenge = b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
|
|
2586
|
+
const callback2 = `${basePath}/sso/callback`;
|
|
2587
|
+
const remember = wants(url.searchParams.get("remember"));
|
|
2588
|
+
const started = await ask(request, "POST", "/v1/auth/sso/start", {
|
|
2589
|
+
...connection ? { connection } : { email },
|
|
2590
|
+
redirect_uri: `${originOf(request)}${callback2}`,
|
|
2591
|
+
code_challenge: challenge
|
|
2592
|
+
}, callback2, void 0, remember);
|
|
2593
|
+
if (started.error || typeof started.data?.redirect_url !== "string") return failed2(started.error?.code ?? "sso_unavailable");
|
|
2594
|
+
const step = { kind: "sso", token: verifier, returnTo, ...remember ? { remember } : {} };
|
|
2595
|
+
return redirect(started.data.redirect_url, [writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, STEP_SECONDS))]);
|
|
2596
|
+
}
|
|
2597
|
+
if (route === "/sso/callback" && !post) {
|
|
2598
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
2599
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2600
|
+
const clear = writeCookie(stepCookie, "", attrs(request, 0));
|
|
2601
|
+
const back = step?.returnTo ?? afterSignIn;
|
|
2602
|
+
const failedTo = (code2) => redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code2)}`, [clear]);
|
|
2603
|
+
if (step === null || step.kind !== "sso") return failedTo("state_mismatch");
|
|
2604
|
+
const refused = url.searchParams.get("error");
|
|
2605
|
+
if (refused) return failedTo(refused);
|
|
2606
|
+
const code = url.searchParams.get("code");
|
|
2607
|
+
if (!code) return failedTo("sso_failed");
|
|
2608
|
+
const done = await ask(request, "POST", "/v1/auth/sso/token", { code, redirect_uri: `${originOf(request)}${basePath}/sso/callback`, code_verifier: step.token }, void 0, void 0, step.remember === true);
|
|
2609
|
+
if (done.error) return failedTo(done.error.code);
|
|
2610
|
+
const s = await stored(done.data);
|
|
2611
|
+
if (s === null) return failedTo("unavailable");
|
|
2612
|
+
return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
|
|
2613
|
+
}
|
|
2614
|
+
if (route.startsWith("/sign-in/social/") && !post) {
|
|
2615
|
+
const provider = providerIn(route.slice("/sign-in/social/".length));
|
|
2616
|
+
if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
|
|
2617
|
+
const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
2618
|
+
const callback2 = `${basePath}/callback/${provider}`;
|
|
2619
|
+
const remember = wants(url.searchParams.get("remember"));
|
|
2620
|
+
const started = await ask(request, "GET", `/v1/auth/social/${provider}?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2, void 0, remember);
|
|
2621
|
+
if (started.error || typeof started.data?.redirect_url !== "string") {
|
|
2622
|
+
return redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(started.error?.code ?? "provider_unavailable")}`);
|
|
2623
|
+
}
|
|
2624
|
+
const step = { kind: "social", token: String(started.data.state ?? ""), provider, returnTo, ...remember ? { remember } : {} };
|
|
2625
|
+
return redirect(started.data.redirect_url, [writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, STEP_SECONDS))]);
|
|
2626
|
+
}
|
|
2627
|
+
if (route.startsWith("/callback/") && !post) {
|
|
2628
|
+
const provider = providerIn(route.slice("/callback/".length));
|
|
2629
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
2630
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2631
|
+
const clear = writeCookie(stepCookie, "", attrs(request, 0));
|
|
2632
|
+
const back = step?.returnTo ?? afterSignIn;
|
|
2633
|
+
const failedTo = (code2) => redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code2)}`, [clear]);
|
|
2634
|
+
if (step === null || step.kind !== "social" && step.kind !== "link" && step.kind !== "upgrade" || step.provider !== provider || url.searchParams.get("state") !== step.token) return failedTo("state_mismatch");
|
|
2635
|
+
const code = url.searchParams.get("code");
|
|
2636
|
+
if (!code) return failedTo(url.searchParams.get("error") === "access_denied" ? "cancelled" : "provider_refused");
|
|
2637
|
+
const q = new URLSearchParams({ code, state: step.token, redirect_uri: `${originOf(request)}${basePath}/callback/${provider}` });
|
|
2638
|
+
if (step.kind === "upgrade") {
|
|
2639
|
+
const mine = await signedInAs(request);
|
|
2640
|
+
if ("response" in mine) return failedTo("unauthenticated");
|
|
2641
|
+
const done2 = await ask(request, "GET", `/v1/auth/anonymous/upgrade/${provider}/callback?${q}`, void 0, void 0, mine.token);
|
|
2642
|
+
if (done2.error) return failedTo(done2.error.code);
|
|
2643
|
+
const s2 = await stored(done2.data, (await read(request)).value ?? void 0);
|
|
2644
|
+
if (s2 === null) return failedTo("unavailable");
|
|
2645
|
+
return redirect(back, [...await write(request, s2, readCookies(request.headers.get("cookie"))), clear]);
|
|
2646
|
+
}
|
|
2647
|
+
if (step.kind === "link") {
|
|
2648
|
+
const mine = await signedInAs(request);
|
|
2649
|
+
if ("response" in mine) return failedTo("unauthenticated");
|
|
2650
|
+
const linked = await ask(request, "GET", `/v1/auth/identities/${provider}/link/callback?${q}`, void 0, void 0, mine.token);
|
|
2651
|
+
if (linked.error) return failedTo(linked.error.code);
|
|
2652
|
+
const h = new Headers(mine.headers);
|
|
2653
|
+
h.append("set-cookie", clear);
|
|
2654
|
+
return redirect(`${back}${back.includes("?") ? "&" : "?"}linked=${encodeURIComponent(provider)}`, [...h.getSetCookie()]);
|
|
2655
|
+
}
|
|
2656
|
+
const done = await ask(request, "GET", `/v1/auth/social/${provider}/callback?${q}`, void 0, void 0, void 0, step.remember === true);
|
|
2657
|
+
if (done.error) return failedTo(done.error.code);
|
|
2658
|
+
const s = await stored(done.data);
|
|
2659
|
+
if (s === null) return failedTo("unavailable");
|
|
2660
|
+
return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
|
|
2661
|
+
}
|
|
2662
|
+
if (route.startsWith("/me/upgrade/social/") && !post) {
|
|
2663
|
+
const provider = providerIn(route.slice("/me/upgrade/social/".length));
|
|
2664
|
+
if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
|
|
2665
|
+
const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
2666
|
+
const failed2 = (code) => redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code)}`);
|
|
2667
|
+
const mine = await signedInAs(request);
|
|
2668
|
+
if ("response" in mine) return failed2("unauthenticated");
|
|
2669
|
+
const callback2 = `${basePath}/callback/${provider}`;
|
|
2670
|
+
const started = await ask(request, "GET", `/v1/auth/anonymous/upgrade/${provider}?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2, mine.token);
|
|
2671
|
+
if (started.error || typeof started.data?.redirect_url !== "string") return failed2(started.error?.code ?? "provider_unavailable");
|
|
2672
|
+
const step = { kind: "upgrade", token: String(started.data.state ?? ""), provider, returnTo };
|
|
2673
|
+
return redirect(started.data.redirect_url, [...mine.headers.getSetCookie(), writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, STEP_SECONDS))]);
|
|
2674
|
+
}
|
|
2675
|
+
if (route.startsWith("/me/identities/link/") && !post) {
|
|
2676
|
+
const provider = providerIn(route.slice("/me/identities/link/".length));
|
|
2677
|
+
if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
|
|
2678
|
+
const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
2679
|
+
const failed2 = (code) => redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code)}`);
|
|
2680
|
+
const mine = await signedInAs(request);
|
|
2681
|
+
if ("response" in mine) return failed2("unauthenticated");
|
|
2682
|
+
const callback2 = `${basePath}/callback/${provider}`;
|
|
2683
|
+
const started = await ask(request, "GET", `/v1/auth/identities/${provider}/link?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2, mine.token);
|
|
2684
|
+
if (started.error || typeof started.data?.redirect_url !== "string") return failed2(started.error?.code ?? "provider_unavailable");
|
|
2685
|
+
const step = { kind: "link", token: String(started.data.state ?? ""), provider, returnTo };
|
|
2686
|
+
return redirect(started.data.redirect_url, [...mine.headers.getSetCookie(), writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, STEP_SECONDS))]);
|
|
2687
|
+
}
|
|
2688
|
+
if (route === "/me/identities" && !post) {
|
|
2689
|
+
const mine = await signedInAs(request);
|
|
2690
|
+
if ("response" in mine) return mine.response;
|
|
2691
|
+
const res = await ask(request, "GET", "/v1/auth/identities", void 0, void 0, mine.token);
|
|
2692
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
2693
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
2694
|
+
return json({ identities: list.map((x) => ({ provider: String(x.provider ?? ""), email: String(x.email ?? ""), name: String(x.name ?? ""), linkedAt: String(x.linked_at ?? "") })) }, 200, mine.headers);
|
|
2695
|
+
}
|
|
2696
|
+
if (route === "/me/passkeys" && !post) {
|
|
2697
|
+
const mine = await signedInAs(request);
|
|
2698
|
+
if ("response" in mine) return mine.response;
|
|
2699
|
+
const res = await ask(request, "GET", "/v1/auth/webauthn/credentials", void 0, void 0, mine.token);
|
|
2700
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
2701
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
2702
|
+
return json({ passkeys: list.map(passkeyOf) }, 200, mine.headers);
|
|
2703
|
+
}
|
|
2704
|
+
if (route === "/me/agents" && !post) {
|
|
2705
|
+
const mine = await signedInAs(request);
|
|
2706
|
+
if ("response" in mine) return mine.response;
|
|
2707
|
+
const res = await ask(request, "GET", "/v1/auth/me/agents", void 0, void 0, mine.token);
|
|
2708
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
2709
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
2710
|
+
return json({ agents: list.map(agentOf) }, 200, mine.headers);
|
|
2711
|
+
}
|
|
2712
|
+
if ((route === "/me/organizations" || route === "/me/organizations/members" || route === "/me/organizations/roles") && !post) {
|
|
2713
|
+
const mine = await signedInAs(request);
|
|
2714
|
+
if ("response" in mine) return mine.response;
|
|
2715
|
+
let path = "/v1/auth/me/organizations";
|
|
2716
|
+
if (route !== "/me/organizations") {
|
|
2717
|
+
const id = url.searchParams.get("id") ?? "";
|
|
2718
|
+
if (!UUID3.test(id)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
2719
|
+
path += `/${id}/${route.endsWith("/roles") ? "roles" : "members"}`;
|
|
2720
|
+
} else if (url.searchParams.get("system")) {
|
|
2721
|
+
path += `?system=${encodeURIComponent(url.searchParams.get("system"))}`;
|
|
2722
|
+
}
|
|
2723
|
+
const res = await ask(request, "GET", path, void 0, void 0, mine.token);
|
|
2724
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
2725
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
2726
|
+
if (route === "/me/organizations") return json({ organizations: list.map(seatOf) }, 200, mine.headers);
|
|
2727
|
+
if (route === "/me/organizations/roles") return json({ roles: list.map(orgRoleOf) }, 200, mine.headers);
|
|
2728
|
+
return json({ members: list.map(orgMemberOf) }, 200, mine.headers);
|
|
2729
|
+
}
|
|
2730
|
+
if (route === "/me/mfa/factors" && !post) {
|
|
2731
|
+
const mine = await signedInAs(request);
|
|
2732
|
+
if ("response" in mine) return mine.response;
|
|
2733
|
+
const res = await ask(request, "GET", "/v1/auth/mfa/factors", void 0, void 0, mine.token);
|
|
2734
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
2735
|
+
return json(factorsOf(res.data), 200, mine.headers);
|
|
2736
|
+
}
|
|
2737
|
+
if ((route === "/me" || route === "/me/sessions" || route === "/me/legal") && !post) {
|
|
2738
|
+
const mine = await signedInAs(request);
|
|
2739
|
+
if ("response" in mine) return mine.response;
|
|
2740
|
+
const res = await ask(request, "GET", route === "/me" ? "/v1/auth/me" : route === "/me/legal" ? "/v1/auth/me/legal" : "/v1/auth/me/sessions", void 0, void 0, mine.token);
|
|
2741
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
2742
|
+
if (route === "/me/legal") {
|
|
2743
|
+
const list = Array.isArray(res.data?.pending) ? res.data.pending : [];
|
|
2744
|
+
return json({ pending: list.map((d) => ({ kind: String(d.kind ?? ""), version: String(d.version ?? ""), url: String(d.url ?? "") })) }, 200, mine.headers);
|
|
2745
|
+
}
|
|
2746
|
+
if (route === "/me/sessions") {
|
|
2747
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
2748
|
+
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);
|
|
2749
|
+
}
|
|
2750
|
+
return json({ account: accountOf(res.data) }, 200, mine.headers);
|
|
2751
|
+
}
|
|
2752
|
+
if (route === "/sign-in/mfa/push/events" && !post) {
|
|
2753
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
2754
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2755
|
+
if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
|
|
2756
|
+
let upstream;
|
|
2757
|
+
try {
|
|
2758
|
+
upstream = await doFetch(`${issuer}/v1/auth/mfa/push/events`, {
|
|
2759
|
+
headers: { "x-tenant-key": tenant, "x-mfa-token": step.token, accept: "text/event-stream" },
|
|
2760
|
+
signal: request.signal
|
|
2761
|
+
});
|
|
2762
|
+
} catch {
|
|
2763
|
+
return problem(503, "unavailable", "Cauthy could not be reached.");
|
|
2764
|
+
}
|
|
2765
|
+
if (!upstream.ok || !upstream.body) {
|
|
2766
|
+
const b2 = await upstream.json().catch(() => null);
|
|
2767
|
+
return problem(upstream.status, String(b2?.errors?.[0]?.code ?? "error"), String(b2?.errors?.[0]?.message ?? "The request could not be completed."));
|
|
2768
|
+
}
|
|
2769
|
+
return new Response(upstream.body, {
|
|
2770
|
+
status: 200,
|
|
2771
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache, no-store", "x-accel-buffering": "no" }
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
if (!post) return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
2775
|
+
if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
|
|
2776
|
+
if (route === "/sign-out") {
|
|
2777
|
+
const { had, value } = await read(request);
|
|
2778
|
+
if (value !== null) {
|
|
2779
|
+
const form = new URLSearchParams({ client_id: clientId, token: value.rt });
|
|
2780
|
+
if (clientSecret) form.set("client_secret", clientSecret);
|
|
2781
|
+
await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
|
|
2782
|
+
orgCache.delete(value.at);
|
|
2783
|
+
}
|
|
2784
|
+
const h = new Headers();
|
|
2785
|
+
for (const c of clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
|
|
2786
|
+
return json({ status: "signed_out" }, 200, h);
|
|
2787
|
+
}
|
|
2788
|
+
const b = await body(request);
|
|
2789
|
+
if (b === null) return problem(400, "bad_request", "Send the form as JSON.");
|
|
2790
|
+
if (route === "/sign-in/sso/discover") {
|
|
2791
|
+
const bad = checkEmail(b.email);
|
|
2792
|
+
if (bad) return invalid(bad);
|
|
2793
|
+
const res = await ask(request, "POST", "/v1/auth/sso/discover", { email: String(b.email).trim() });
|
|
2794
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
2795
|
+
const c = res.data?.connection;
|
|
2796
|
+
return json(c ? { status: "found", connection: { id: String(c.slug ?? ""), name: String(c.display_name ?? ""), enforced: c.enforced === true } } : { status: "none" });
|
|
2797
|
+
}
|
|
2798
|
+
if (route === "/sign-in/anonymous") {
|
|
2799
|
+
if (!post) return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
2800
|
+
const res = await ask(request, "POST", "/v1/auth/anonymous", {});
|
|
2801
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
2802
|
+
return signedIn(request, res.data);
|
|
2803
|
+
}
|
|
2804
|
+
if (route === "/sign-in/email") {
|
|
2805
|
+
const bad = firstProblem(checkEmail(b.email), checkPassword(b.password));
|
|
2806
|
+
if (bad) return invalid(bad);
|
|
2807
|
+
const remember = wants(b.remember);
|
|
2808
|
+
const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password }, void 0, void 0, remember);
|
|
2809
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
2810
|
+
return loggedIn(request, res.data, remember);
|
|
2811
|
+
}
|
|
2812
|
+
if (route === "/sign-in/mfa") {
|
|
2813
|
+
const bad = checkCode(b.code);
|
|
2814
|
+
if (bad) return invalid(bad);
|
|
2815
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
2816
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2817
|
+
if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
|
|
2818
|
+
const res = await ask(request, "POST", "/v1/auth/mfa/challenge", { mfa_token: step.token, code: String(b.code).trim() }, void 0, void 0, step.remember === true);
|
|
2819
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
|
|
2820
|
+
return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
|
|
2821
|
+
}
|
|
2822
|
+
if (route === "/sign-in/mfa/push" || route === "/sign-in/mfa/push/finish") {
|
|
2823
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
2824
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2825
|
+
if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
|
|
2826
|
+
if (route === "/sign-in/mfa/push") {
|
|
2827
|
+
const res2 = await ask(request, "POST", "/v1/auth/mfa/push", { mfa_token: step.token }, void 0, void 0, step.remember === true);
|
|
2828
|
+
if (res2.error) return problem(res2.status, res2.error.code, res2.error.message);
|
|
2829
|
+
return json({ status: "sent", number: Number(res2.data?.number ?? 0) });
|
|
2830
|
+
}
|
|
2831
|
+
const res = await ask(request, "POST", "/v1/auth/mfa/challenge", { mfa_token: step.token, push: true }, void 0, void 0, step.remember === true);
|
|
2832
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
2833
|
+
return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
|
|
2834
|
+
}
|
|
2835
|
+
if (route === "/sign-in/mfa/methods" || route === "/sign-in/mfa/send" || route === "/sign-in/mfa/email") {
|
|
2836
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
2837
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2838
|
+
if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
|
|
2839
|
+
if (route === "/sign-in/mfa/methods") {
|
|
2840
|
+
const res2 = await ask(request, "POST", "/v1/auth/mfa/methods", { mfa_token: step.token }, void 0, void 0, step.remember === true);
|
|
2841
|
+
if (res2.error) return problem(res2.status, res2.error.code, res2.error.message);
|
|
2842
|
+
const p = res2.data?.phone;
|
|
2843
|
+
const push = res2.data?.push;
|
|
2844
|
+
const mail = res2.data?.email;
|
|
2845
|
+
return json({
|
|
2846
|
+
status: "ok",
|
|
2847
|
+
totp: res2.data?.totp === true,
|
|
2848
|
+
phone: p ? { number: String(p.number ?? ""), channel: String(p.channel ?? "sms") } : null,
|
|
2849
|
+
push: push ? { devices: Number(push.devices ?? 0) } : null,
|
|
2850
|
+
email: mail ? { to: String(mail.to ?? "") } : null
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
if (route === "/sign-in/mfa/email") {
|
|
2854
|
+
const res2 = await ask(request, "POST", "/v1/auth/mfa/email", { mfa_token: step.token }, void 0, void 0, step.remember === true);
|
|
2855
|
+
if (res2.error) return problem(res2.status, res2.error.code, res2.error.message);
|
|
2856
|
+
return json({ status: "sent", sentTo: String(res2.data?.sent_to ?? "") });
|
|
2857
|
+
}
|
|
2858
|
+
const res = await ask(request, "POST", "/v1/auth/mfa/send", { mfa_token: step.token }, void 0, void 0, step.remember === true);
|
|
2859
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
2860
|
+
return json({ status: "sent", sentTo: String(res.data?.sent_to ?? ""), channel: String(res.data?.channel ?? "sms") });
|
|
2861
|
+
}
|
|
2862
|
+
if (route === "/sign-up/email") {
|
|
2863
|
+
const bad = firstProblem(checkName(b.name), checkEmail(b.email), await newPasswordProblem(request, b.password), checkOrganization(b.organization));
|
|
2864
|
+
if (bad) return invalid(bad);
|
|
2865
|
+
const email = String(b.email).trim();
|
|
2866
|
+
const organization = typeof b.organization === "string" && b.organization.trim() !== "" ? { organization: { name: b.organization.trim() } } : {};
|
|
2867
|
+
const legal = { ...b.acceptLegal === true ? { accept_legal: true } : {}, ...b.consents && typeof b.consents === "object" ? { consents: b.consents } : {} };
|
|
2868
|
+
const made = await ask(request, "POST", "/v1/auth/register", { email, password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "", ...organization, ...legal }, pages.verifyEmail);
|
|
2869
|
+
if (made.error) return problem(made.status, made.error.code, made.error.message, made.error.code === "weak_password" ? "password" : void 0, void 0, made.error.reasons);
|
|
2870
|
+
if (made.data?.verification_required === true) return json({ status: "verification_required", email });
|
|
2871
|
+
const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
|
|
2872
|
+
if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
|
|
2873
|
+
return signedIn(request, res.data);
|
|
2874
|
+
}
|
|
2875
|
+
if (route === "/forgot-password") {
|
|
2876
|
+
const bad = checkEmail(b.email);
|
|
2877
|
+
if (bad) return invalid(bad);
|
|
2878
|
+
await ask(request, "POST", "/v1/auth/password/reset", { email: String(b.email).trim() }, pages.resetPassword);
|
|
2879
|
+
return json({ status: "sent" });
|
|
2880
|
+
}
|
|
2881
|
+
if (route.startsWith("/me")) {
|
|
2882
|
+
const mine = await signedInAs(request);
|
|
2883
|
+
if ("response" in mine) return mine.response;
|
|
2884
|
+
const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
|
|
2885
|
+
const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers, res.error.reasons);
|
|
2886
|
+
const password = typeof b.password === "string" ? b.password : "";
|
|
2887
|
+
if (route === "/me") {
|
|
2888
|
+
const bad = checkName(b.name);
|
|
2889
|
+
if (bad) return invalid(bad);
|
|
2890
|
+
const res = await as("/v1/auth/me", { name: typeof b.name === "string" ? b.name.trim() : "" }, "PATCH");
|
|
2891
|
+
if (res.error) return refused(res, "name");
|
|
2892
|
+
const h = mine.headers;
|
|
2893
|
+
if (mine.kept !== null) {
|
|
2894
|
+
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)) }, mine.kept);
|
|
2895
|
+
if (again !== null) for (const c of await write(request, again, readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
|
|
2896
|
+
}
|
|
2897
|
+
return json({ status: "saved", account: accountOf(res.data) }, 200, h);
|
|
2898
|
+
}
|
|
2899
|
+
if (route === "/me/legal/accept") {
|
|
2900
|
+
const kinds = Array.isArray(b.kinds) ? b.kinds.filter((k) => typeof k === "string") : [];
|
|
2901
|
+
const res = await as("/v1/auth/me/legal/accept", { kinds });
|
|
2902
|
+
if (res.error) return refused(res);
|
|
2903
|
+
return json({ status: "accepted" }, 200, mine.headers);
|
|
2904
|
+
}
|
|
2905
|
+
if (route === "/me/password") {
|
|
2906
|
+
const bad = await newPasswordProblem(request, b.newPassword);
|
|
2907
|
+
if (bad) return problem(400, bad.code, bad.message, "newPassword");
|
|
2908
|
+
const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
|
|
2909
|
+
if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
|
|
2910
|
+
const next = await stored(res.data, (await read(request)).value ?? void 0);
|
|
2911
|
+
const h = new Headers();
|
|
2912
|
+
const had = readCookies(request.headers.get("cookie"));
|
|
2913
|
+
for (const c of next !== null ? await write(request, next, had) : clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
|
|
2914
|
+
return json({ status: "changed", signedIn: next !== null }, 200, h);
|
|
2915
|
+
}
|
|
2916
|
+
if (route === "/me/mfa/setup") {
|
|
2917
|
+
const res = await as("/v1/auth/mfa/setup", { password });
|
|
2918
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
2919
|
+
const qr = String(res.data?.qr ?? "");
|
|
2920
|
+
return json({ status: "started", secret: String(res.data?.secret ?? ""), otpauthUri: String(res.data?.otpauth_uri ?? ""), qr: qr.startsWith("data:image/png;base64,") ? qr : "" }, 200, mine.headers);
|
|
2921
|
+
}
|
|
2922
|
+
if (route === "/me/mfa/verify") {
|
|
2923
|
+
const bad = checkCode(b.code);
|
|
2924
|
+
if (bad) return invalid(bad);
|
|
2925
|
+
const res = await as("/v1/auth/mfa/verify", { code: String(b.code).trim() });
|
|
2926
|
+
if (res.error) return refused(res, "code");
|
|
2927
|
+
return json({ status: "enabled", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
|
|
2928
|
+
}
|
|
2929
|
+
if (route === "/me/mfa/phone") {
|
|
2930
|
+
const res = await as("/v1/auth/mfa/phone", { phone: typeof b.phone === "string" ? b.phone : "", channel: typeof b.channel === "string" ? b.channel : "sms", password });
|
|
2931
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : res.error.code === "validation_error" ? "phone" : void 0);
|
|
2932
|
+
return json({ status: "sent", sentTo: String(res.data?.sent_to ?? ""), channel: String(res.data?.channel ?? "") }, 200, mine.headers);
|
|
2933
|
+
}
|
|
2934
|
+
if (route === "/me/mfa/phone/verify") {
|
|
2935
|
+
const bad = checkCode(b.code);
|
|
2936
|
+
if (bad) return invalid(bad);
|
|
2937
|
+
const res = await as("/v1/auth/mfa/phone/verify", { code: String(b.code).trim() });
|
|
2938
|
+
if (res.error) return refused(res, "code");
|
|
2939
|
+
return json({ status: "enabled", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
|
|
2940
|
+
}
|
|
2941
|
+
if (route === "/me/mfa/phone/remove") {
|
|
2942
|
+
const res = await as("/v1/auth/mfa/phone/remove", { password });
|
|
2943
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
2944
|
+
return json({ status: "removed" }, 200, mine.headers);
|
|
2945
|
+
}
|
|
2946
|
+
if (route === "/me/mfa/disable") {
|
|
2947
|
+
const res = await as("/v1/auth/me/mfa/disable", { password });
|
|
2948
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
2949
|
+
return json({ status: "disabled" }, 200, mine.headers);
|
|
2950
|
+
}
|
|
2951
|
+
if (route === "/me/mfa/recovery-codes") {
|
|
2952
|
+
const res = await as("/v1/auth/me/mfa/recovery-codes", { password });
|
|
2953
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
2954
|
+
return json({ status: "renewed", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
|
|
2955
|
+
}
|
|
2956
|
+
if (route === "/me/email") {
|
|
2957
|
+
const bad = checkEmail(b.email);
|
|
2958
|
+
if (bad) return invalid(bad);
|
|
2959
|
+
const email = String(b.email).trim();
|
|
2960
|
+
const res = await as("/v1/auth/me/email", { email, password });
|
|
2961
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : ["invalid_email", "email_unchanged", "email_taken"].includes(res.error.code) ? "email" : void 0);
|
|
2962
|
+
return json({ status: "sent", email }, 200, mine.headers);
|
|
2963
|
+
}
|
|
2964
|
+
if (route === "/me/identities/unlink") {
|
|
2965
|
+
const provider = typeof b.provider === "string" ? b.provider : "";
|
|
2966
|
+
if (!PROVIDER_ID.test(provider)) return problem(400, "invalid_provider", "Say which provider to unlink.", "provider", mine.headers);
|
|
2967
|
+
const res = await as(`/v1/auth/identities/${provider}`, void 0, "DELETE");
|
|
2968
|
+
if (res.error) return refused(res);
|
|
2969
|
+
return json({ status: "unlinked", provider }, 200, mine.headers);
|
|
2970
|
+
}
|
|
2971
|
+
if (route === "/me/passkeys/add/begin") {
|
|
2972
|
+
const kind = b.kind === "device" || b.kind === "security_key" ? b.kind : "";
|
|
2973
|
+
const res = await as("/v1/auth/webauthn/register/begin", { password, ...kind ? { kind } : {} });
|
|
2974
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
2975
|
+
const step = { kind: "enrol", token: String(res.data?.session_id ?? ""), returnTo: afterSignIn };
|
|
2976
|
+
const h = new Headers(mine.headers);
|
|
2977
|
+
h.append("set-cookie", writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, PASSKEY_SECONDS)));
|
|
2978
|
+
return json({ status: "started", options: res.data?.options?.publicKey ?? res.data?.options ?? null }, 200, h);
|
|
2979
|
+
}
|
|
2980
|
+
if (route === "/me/passkeys/add/finish") {
|
|
2981
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
2982
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
2983
|
+
if (step === null || step.kind !== "enrol") return problem(400, "step_expired", "Start adding the passkey again: it was asked for too long ago.", void 0, mine.headers);
|
|
2984
|
+
if (!isCredential(b.credential)) return problem(400, "bad_request", "Send the passkey the browser made.", void 0, mine.headers);
|
|
2985
|
+
const name = typeof b.name === "string" ? b.name.trim().slice(0, 60) : "";
|
|
2986
|
+
const res = await as("/v1/auth/webauthn/register/finish", { session_id: step.token, nickname: name, credential: b.credential });
|
|
2987
|
+
const h = new Headers(mine.headers);
|
|
2988
|
+
h.append("set-cookie", writeCookie(stepCookie, "", attrs(request, 0)));
|
|
2989
|
+
if (res.error) return problem(res.status, res.error.code === "attestation_invalid" ? "passkey_failed" : res.error.code, res.error.code === "attestation_invalid" ? "The passkey could not be added. Try again." : res.error.message, void 0, h);
|
|
2990
|
+
return json({ status: "added", passkey: passkeyOf(res.data) }, 200, h);
|
|
2991
|
+
}
|
|
2992
|
+
if (route === "/me/passkeys/remove") {
|
|
2993
|
+
const id = typeof b.id === "string" ? b.id : "";
|
|
2994
|
+
if (!/^[0-9a-f-]{36}$/i.test(id)) return problem(400, "invalid_passkey", "Say which passkey to remove.", void 0, mine.headers);
|
|
2995
|
+
const res = await as(`/v1/auth/webauthn/credentials/${id}`, void 0, "DELETE");
|
|
2996
|
+
if (res.error) return refused(res);
|
|
2997
|
+
return json({ status: "removed", id }, 200, mine.headers);
|
|
2998
|
+
}
|
|
2999
|
+
if (route === "/me/upgrade") {
|
|
3000
|
+
const bad = firstProblem(checkName(b.name), checkEmail(b.email), await newPasswordProblem(request, b.password));
|
|
3001
|
+
if (bad) return invalid(bad);
|
|
3002
|
+
const res = await as("/v1/auth/anonymous/upgrade", { email: String(b.email).trim(), password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "" });
|
|
3003
|
+
if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
|
|
3004
|
+
return signedIn(request, res.data, [], {}, (await read(request)).value ?? void 0);
|
|
3005
|
+
}
|
|
3006
|
+
if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
|
|
3007
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3008
|
+
if (!UUID3.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3009
|
+
const path = `/v1/auth/me/organizations/${org}`;
|
|
3010
|
+
if (route === "/me/organizations/rename") {
|
|
3011
|
+
const bad = checkOrganizationName(b.name);
|
|
3012
|
+
if (bad) return invalid(bad);
|
|
3013
|
+
const res2 = await as(path, { name: String(b.name).trim() }, "PATCH");
|
|
3014
|
+
if (res2.error) return refused(res2, res2.error.code === "validation_error" ? "name" : void 0);
|
|
3015
|
+
orgCache.delete(mine.token);
|
|
3016
|
+
return json({ status: "renamed", organization: seatOf(res2.data) }, 200, mine.headers);
|
|
3017
|
+
}
|
|
3018
|
+
const res = route === "/me/organizations/archive" ? await as(`${path}/archive`) : await as(path, void 0, "DELETE");
|
|
3019
|
+
if (res.error) return refused(res);
|
|
3020
|
+
orgCache.delete(mine.token);
|
|
3021
|
+
return json({ status: route === "/me/organizations/archive" ? "archived" : "deleted", organizationId: org }, 200, mine.headers);
|
|
3022
|
+
}
|
|
3023
|
+
if (route.startsWith("/me/organizations/members/")) {
|
|
3024
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3025
|
+
if (!UUID3.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3026
|
+
const base = `/v1/auth/me/organizations/${org}/members`;
|
|
3027
|
+
if (route === "/me/organizations/members/add") {
|
|
3028
|
+
const bad = checkEmail(b.email);
|
|
3029
|
+
if (bad) return invalid(bad);
|
|
3030
|
+
const role = typeof b.role === "string" ? b.role : "";
|
|
3031
|
+
if (role === "") return problem(400, "invalid_role", "Choose a role.", "role", mine.headers);
|
|
3032
|
+
const res = await ask(request, "POST", base, {
|
|
3033
|
+
email: String(b.email).trim(),
|
|
3034
|
+
role,
|
|
3035
|
+
name: typeof b.name === "string" ? b.name.trim() : "",
|
|
3036
|
+
client_id: clientId,
|
|
3037
|
+
only_new: true,
|
|
3038
|
+
...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {}
|
|
3039
|
+
}, pages.acceptInvite, mine.token);
|
|
3040
|
+
if (res.error?.code === "already_member") res.error.code = "member_exists";
|
|
3041
|
+
if (res.error) return refused(res, res.error.code === "unknown_role" ? "role" : "email");
|
|
3042
|
+
return json({ status: "added", member: orgMemberOf(res.data) }, 200, mine.headers);
|
|
3043
|
+
}
|
|
3044
|
+
const user = typeof b.userId === "string" ? b.userId : "";
|
|
3045
|
+
if (!UUID3.test(user)) return problem(400, "invalid_member", "Say which member.", void 0, mine.headers);
|
|
3046
|
+
if (route === "/me/organizations/members/role") {
|
|
3047
|
+
const role = typeof b.role === "string" ? b.role : "";
|
|
3048
|
+
if (role === "") return problem(400, "invalid_role", "Choose a role.", "role", mine.headers);
|
|
3049
|
+
const res = await as(`${base}/${user}`, { role }, "PUT");
|
|
3050
|
+
if (res.error) return refused(res, "role");
|
|
3051
|
+
return json({ status: "changed", userId: user, role }, 200, mine.headers);
|
|
3052
|
+
}
|
|
3053
|
+
if (route === "/me/organizations/members/remove") {
|
|
3054
|
+
const res = await as(`${base}/${user}`, void 0, "DELETE");
|
|
3055
|
+
if (res.error) return refused(res);
|
|
3056
|
+
orgCache.delete(mine.token);
|
|
3057
|
+
return json({ status: "removed", userId: user }, 200, mine.headers);
|
|
3058
|
+
}
|
|
3059
|
+
if (route === "/me/organizations/members/invite") {
|
|
3060
|
+
const res = await ask(request, "POST", `${base}/${user}/invite`, { client_id: clientId, ...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {} }, pages.acceptInvite, mine.token);
|
|
3061
|
+
if (res.error) return refused(res);
|
|
3062
|
+
return json({ status: "sent", userId: user }, 200, mine.headers);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
if (route === "/me/agents/disconnect") {
|
|
3066
|
+
const id = typeof b.id === "string" ? b.id : "";
|
|
3067
|
+
if (!/^[0-9a-f-]{36}$/i.test(id)) return problem(400, "invalid_agent", "Say which agent to disconnect.", void 0, mine.headers);
|
|
3068
|
+
const res = await as(`/v1/auth/me/agents/${id}`, void 0, "DELETE");
|
|
3069
|
+
if (res.error) return refused(res);
|
|
3070
|
+
return json({ status: "disconnected", id }, 200, mine.headers);
|
|
3071
|
+
}
|
|
3072
|
+
if (route === "/me/sessions/end") {
|
|
3073
|
+
const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
|
|
3074
|
+
if (res.error) return refused(res);
|
|
3075
|
+
const h = new Headers();
|
|
3076
|
+
for (const c of clearParts(cookieName, attrs(request, 0), readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
|
|
3077
|
+
orgCache.delete(mine.token);
|
|
3078
|
+
return json({ status: "signed_out" }, 200, h);
|
|
3079
|
+
}
|
|
3080
|
+
return null;
|
|
3081
|
+
}
|
|
3082
|
+
const isToken = (v) => typeof v === "string" && v !== "" && v.length <= 512;
|
|
3083
|
+
const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
|
|
3084
|
+
if (route === "/reset-password") {
|
|
3085
|
+
if (!isToken(b.token)) return badLink();
|
|
3086
|
+
const bad = await newPasswordProblem(request, b.password);
|
|
3087
|
+
if (bad) return invalid(bad);
|
|
3088
|
+
const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
|
|
3089
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0, void 0, res.error.reasons);
|
|
3090
|
+
return json({ status: "changed" });
|
|
3091
|
+
}
|
|
3092
|
+
if (route === "/verify-email") {
|
|
3093
|
+
if (!isToken(b.token)) return badLink();
|
|
3094
|
+
const res = await ask(request, "POST", "/v1/auth/email/verify", { token: String(b.token) });
|
|
3095
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
3096
|
+
if (res.data?.access_token) return signedIn(request, res.data);
|
|
3097
|
+
return json({ status: "verified" });
|
|
3098
|
+
}
|
|
3099
|
+
if (route === "/invitation/accept") {
|
|
3100
|
+
if (!isToken(b.token)) return problem(422, "invalid_token", "This invitation is not valid, or it has expired.");
|
|
3101
|
+
const bad = await newPasswordProblem(request, b.password);
|
|
3102
|
+
if (bad) return invalid(bad);
|
|
3103
|
+
const res = await ask(request, "POST", "/v1/auth/invitation/accept", { token: String(b.token), password: b.password });
|
|
3104
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0, void 0, res.error.reasons);
|
|
3105
|
+
return loggedIn(request, res.data);
|
|
3106
|
+
}
|
|
3107
|
+
if (route === "/sign-in/passkey/begin") {
|
|
3108
|
+
const email = typeof b.email === "string" && checkEmail(b.email) === null ? b.email.trim() : "";
|
|
3109
|
+
const remember = wants(b.remember);
|
|
3110
|
+
const res = await ask(request, "POST", "/v1/auth/webauthn/login/begin", email !== "" ? { email } : {}, void 0, void 0, remember);
|
|
3111
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
3112
|
+
const step = { kind: "passkey", token: String(res.data?.session_id ?? ""), returnTo: afterSignIn, ...remember ? { remember } : {} };
|
|
3113
|
+
return json(
|
|
3114
|
+
{ status: "started", options: res.data?.options?.publicKey ?? res.data?.options ?? null },
|
|
3115
|
+
200,
|
|
3116
|
+
new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, PASSKEY_SECONDS)) })
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
if (route === "/sign-in/passkey/finish") {
|
|
3120
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
3121
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
3122
|
+
if (step === null || step.kind !== "passkey") return problem(400, "step_expired", "Try the passkey again: it was asked for too long ago.");
|
|
3123
|
+
if (!isCredential(b.credential)) return problem(400, "bad_request", "Send the passkey the browser answered with.");
|
|
3124
|
+
const res = await ask(request, "POST", "/v1/auth/webauthn/login/finish", { session_id: step.token, credential: b.credential }, void 0, void 0, step.remember === true || wants(b.remember));
|
|
3125
|
+
const clear = writeCookie(stepCookie, "", attrs(request, 0));
|
|
3126
|
+
if (res.error) {
|
|
3127
|
+
const failed2 = ["assertion_invalid", "session_expired", "clone_detected", "not_found"].includes(res.error.code);
|
|
3128
|
+
return problem(res.status, failed2 ? "passkey_failed" : res.error.code, failed2 ? "The passkey sign-in could not be completed. Try again, or sign in another way." : res.error.message, void 0, new Headers({ "set-cookie": clear }));
|
|
3129
|
+
}
|
|
3130
|
+
return signedIn(request, res.data, [clear]);
|
|
3131
|
+
}
|
|
3132
|
+
if (route === "/sign-in/google-one-tap/begin") {
|
|
3133
|
+
const res = await ask(request, "POST", "/v1/auth/google/one-tap/begin", {});
|
|
3134
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
3135
|
+
return json({ status: "started", clientId: String(res.data?.client_id ?? ""), nonce: String(res.data?.nonce ?? "") });
|
|
3136
|
+
}
|
|
3137
|
+
if (route === "/sign-in/google-one-tap") {
|
|
3138
|
+
if (typeof b.credential !== "string" || b.credential === "") return problem(400, "bad_request", "Send the credential Google gave the page.");
|
|
3139
|
+
const res = await ask(request, "POST", "/v1/auth/google/one-tap", { credential: b.credential }, void 0, void 0, wants(b.remember));
|
|
3140
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
3141
|
+
return signedIn(request, res.data);
|
|
3142
|
+
}
|
|
3143
|
+
if (route === "/sign-in/magic-link") {
|
|
3144
|
+
const bad = checkEmail(b.email);
|
|
3145
|
+
if (bad) return invalid(bad);
|
|
3146
|
+
if (pages.signIn === void 0) return problem(500, "not_configured", "@cauthyhq/node: pages.signIn is required for sign-in links: it is the page the link opens");
|
|
3147
|
+
const email = String(b.email).trim();
|
|
3148
|
+
const back = typeof b.returnTo === "string" ? localPath(b.returnTo, afterSignIn) : afterSignIn;
|
|
3149
|
+
const remember = wants(b.remember);
|
|
3150
|
+
const query = [...back === afterSignIn ? [] : [`return_to=${encodeURIComponent(back)}`], ...remember ? ["remember=1"] : []].join("&");
|
|
3151
|
+
const page = query === "" ? pages.signIn : `${pages.signIn}${pages.signIn.includes("?") ? "&" : "?"}${query}`;
|
|
3152
|
+
await ask(request, "POST", "/v1/auth/magic-link/request", { email, ...b.create === true ? { create_account: true } : {} }, page);
|
|
3153
|
+
const step = { kind: "code", token: email, returnTo: back, ...remember ? { remember } : {} };
|
|
3154
|
+
return json({ status: "sent" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, (await flowKeys)[0]), attrs(request, STEP_SECONDS)) }));
|
|
3155
|
+
}
|
|
3156
|
+
if (route === "/sign-in/email-code") {
|
|
3157
|
+
const bad = checkCode(b.code);
|
|
3158
|
+
if (bad) return invalid(bad);
|
|
3159
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
3160
|
+
const step = sealed ? await unsealAny(sealed, await flowKeys) : null;
|
|
3161
|
+
if (step === null || step.kind !== "code") return problem(400, "step_expired", "Ask for a new link: this one was asked for too long ago.");
|
|
3162
|
+
const res = await ask(request, "POST", "/v1/auth/magic-link/verify-code", { email: step.token, code: String(b.code).trim() }, void 0, void 0, step.remember === true);
|
|
3163
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
|
|
3164
|
+
return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))], { returnTo: step.returnTo });
|
|
3165
|
+
}
|
|
3166
|
+
if (route === "/sign-in/magic-link/verify") {
|
|
3167
|
+
if (!isToken(b.token)) return badLink();
|
|
3168
|
+
const res = await ask(request, "POST", "/v1/auth/magic-link/verify", { token: String(b.token) }, void 0, void 0, wants(b.remember));
|
|
3169
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
3170
|
+
return signedIn(request, res.data);
|
|
3171
|
+
}
|
|
3172
|
+
return null;
|
|
3173
|
+
}
|
|
3174
|
+
async function handler(request) {
|
|
3175
|
+
const url = new URL(request.url);
|
|
3176
|
+
const inside = url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
|
|
3177
|
+
const route = inside ? withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/" : null;
|
|
3178
|
+
const published3 = pact !== void 0 && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD");
|
|
3179
|
+
try {
|
|
3180
|
+
return finish(await serve(request), route, published3);
|
|
3181
|
+
} catch (err) {
|
|
3182
|
+
if (!(err instanceof CauthyApiError && err.code === "email_not_confirmed")) throw err;
|
|
3183
|
+
const wantsPage = request.method === "GET" && (request.headers.get("sec-fetch-mode") ?? "navigate") === "navigate";
|
|
3184
|
+
return finish(wantsPage ? new Response("Sign-in refused (email_not_confirmed). Confirm your email address first.", { status: 403, headers: { "content-type": "text/plain; charset=utf-8" } }) : problem(403, "email_not_confirmed", "Confirm your email address first."), route);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
async function serve(request) {
|
|
3188
|
+
const url = new URL(request.url);
|
|
3189
|
+
if (pact && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD")) return pact.jwks();
|
|
3190
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
|
|
3191
|
+
const route = withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/";
|
|
3192
|
+
const get = request.method === "GET" || request.method === "HEAD";
|
|
3193
|
+
const mine = await ownRoute(request, url, route);
|
|
3194
|
+
if (mine !== null) return mine;
|
|
3195
|
+
if (route === "/me/organizations/check") return checkRoute(request);
|
|
3196
|
+
if (route === "/login" && get) return login(request, url);
|
|
3197
|
+
if (route === "/callback" && get) return callback(request, url);
|
|
3198
|
+
if (route === "/logout" && (get || request.method === "POST")) return logout(request);
|
|
3199
|
+
if (route === "/session" && get) {
|
|
3200
|
+
const r = await resolve2(request);
|
|
3201
|
+
const h = new Headers();
|
|
3202
|
+
for (const c of r.cookies) h.append("set-cookie", c);
|
|
3203
|
+
return json({ session: r.session ? await toPublic(r.session) : null, status: r.status }, 200, h);
|
|
3204
|
+
}
|
|
3205
|
+
if (route === "/refresh" && get) {
|
|
3206
|
+
const r = await resolve2(request);
|
|
3207
|
+
const back = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
3208
|
+
if (r.status === "unauthenticated") return redirect(urls.signIn(back), r.cookies);
|
|
3209
|
+
if (r.status === "expired") return new Response("Sign-in is unavailable right now. Try again in a moment.", { status: 503 });
|
|
3210
|
+
return redirect(back, r.cookies);
|
|
3211
|
+
}
|
|
3212
|
+
if (route === "/account" && get) {
|
|
3213
|
+
const q = new URLSearchParams({ client_id: clientId });
|
|
3214
|
+
const back = localPath(url.searchParams.get("return_to"), "");
|
|
3215
|
+
if (back !== "") q.set("return_to", `${originOf(request)}${back}`);
|
|
3216
|
+
const locale = url.searchParams.get("locale");
|
|
3217
|
+
if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
|
|
3218
|
+
return redirect(`${issuer}/v1/oauth/account?${q}`);
|
|
3219
|
+
}
|
|
3220
|
+
return new Response("Not found", { status: 404 });
|
|
3221
|
+
}
|
|
3222
|
+
async function checkRoute(request) {
|
|
3223
|
+
if (request.method !== "POST") return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
3224
|
+
const refused = withoutRequestHeader(request, "/me/organizations/check");
|
|
3225
|
+
if (refused) return refused;
|
|
3226
|
+
if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
|
|
3227
|
+
const b = await body(request);
|
|
3228
|
+
if (b === null) return problem(400, "bad_request", "Send the question as JSON.");
|
|
3229
|
+
const asker = await signedInAs(request);
|
|
3230
|
+
if ("response" in asker) return asker.response;
|
|
3231
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3232
|
+
if (!UUID3.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
|
|
3233
|
+
const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
|
|
3234
|
+
if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
|
|
3235
|
+
const attributes2 = b.attributes;
|
|
3236
|
+
if (attributes2 !== void 0 && (typeof attributes2 !== "object" || attributes2 === null || Array.isArray(attributes2))) {
|
|
3237
|
+
return problem(400, "bad_request", "The attributes are an object.", void 0, asker.headers);
|
|
3238
|
+
}
|
|
3239
|
+
try {
|
|
3240
|
+
const r = await decide(asker.token, {
|
|
3241
|
+
organization: org,
|
|
3242
|
+
resource: b.resource,
|
|
3243
|
+
action: b.action,
|
|
3244
|
+
...attributes2 !== void 0 ? { attributes: attributes2 } : {},
|
|
3245
|
+
...typeof b.resourceId === "string" ? { resourceId: b.resourceId } : {},
|
|
3246
|
+
...typeof b.ownerId === "string" ? { ownerId: b.ownerId } : {}
|
|
3247
|
+
});
|
|
3248
|
+
return json({ status: "decided", ...r }, 200, asker.headers);
|
|
3249
|
+
} catch (e) {
|
|
3250
|
+
const err = e instanceof CauthyApiError ? e : new CauthyApiError(503, "unavailable", "Cauthy could not be reached.");
|
|
3251
|
+
return problem(err.status, err.code, err.message.replace(/^@cauthyhq\/node: /, ""), void 0, asker.headers);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
async function protect(request) {
|
|
3255
|
+
const read1 = await read(request);
|
|
3256
|
+
const value = read1.value !== null && unverified(read1.value.u) ? null : read1.value;
|
|
3257
|
+
if (value !== null && fresh(value)) return { session: toSession(value) };
|
|
3258
|
+
const url = new URL(request.url);
|
|
3259
|
+
const state = value === null ? "none" : "expired";
|
|
3260
|
+
const page = request.method === "GET" && (request.headers.get("sec-fetch-mode") ?? "navigate") === "navigate" && (request.headers.get("accept") ?? "text/html").includes("text/html");
|
|
3261
|
+
if (!page) {
|
|
3262
|
+
return { response: json({ error: "unauthenticated", session: state }, 401, new Headers({ "x-cauthy-session": state })) };
|
|
3263
|
+
}
|
|
3264
|
+
const back = url.pathname + url.search;
|
|
3265
|
+
const to = value === null ? urls.signIn(back) : `${basePath}/refresh?return_to=${encodeURIComponent(back)}`;
|
|
3266
|
+
return { response: redirect(to) };
|
|
3267
|
+
}
|
|
3268
|
+
const replay = given.webhookReplay ?? memoryReplayStore();
|
|
3269
|
+
const tokenVerifier = (o = {}) => {
|
|
3270
|
+
if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
|
|
3271
|
+
throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
|
|
3272
|
+
}
|
|
3273
|
+
return createTokenVerifier({
|
|
3274
|
+
issuer,
|
|
3275
|
+
tenantId: contract?.tenantId ?? given.tenantId,
|
|
3276
|
+
audience: given.audience,
|
|
3277
|
+
fetch: given.fetch,
|
|
3278
|
+
...contract ? { clientId: contract.clientId } : {},
|
|
3279
|
+
...o
|
|
3280
|
+
});
|
|
3281
|
+
};
|
|
3282
|
+
const webhook = (request, o = {}) => verifyWebhook(request, { issuer, url: o.url ?? `${originOf(request)}${new URL(request.url).pathname}`, jwks: o.jwks, now: o.now, replay });
|
|
3283
|
+
const pact = contract && ownKeys ? {
|
|
3284
|
+
contract,
|
|
3285
|
+
keys: ownKeys,
|
|
3286
|
+
async jwks() {
|
|
3287
|
+
return new Response(JSON.stringify(await ownKeys.published()), {
|
|
3288
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": `public, max-age=${JWKS_MAX_AGE_S}` }
|
|
3289
|
+
});
|
|
3290
|
+
},
|
|
3291
|
+
status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
|
|
3292
|
+
} : void 0;
|
|
3293
|
+
return {
|
|
3294
|
+
handler,
|
|
3295
|
+
checkMount: (target) => checkMount(basePath, target),
|
|
3296
|
+
webhook,
|
|
3297
|
+
tokenVerifier,
|
|
3298
|
+
verifyToken: async (from, o) => tokenVerifier(o)(from),
|
|
3299
|
+
system: systemThatRefreshes,
|
|
3300
|
+
getSession,
|
|
3301
|
+
status,
|
|
3302
|
+
resolve: resolve2,
|
|
3303
|
+
protect,
|
|
3304
|
+
organizations,
|
|
3305
|
+
check,
|
|
3306
|
+
toPublic,
|
|
3307
|
+
urls,
|
|
3308
|
+
...pact ? { pact } : {},
|
|
3309
|
+
options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
function iconFor(issuer) {
|
|
3313
|
+
return (p) => {
|
|
3314
|
+
const raw = typeof p?.icon_url === "string" ? p.icon_url.trim() : "";
|
|
3315
|
+
if (raw === "") return {};
|
|
3316
|
+
try {
|
|
3317
|
+
const url = new URL(raw.startsWith("/") && !raw.startsWith("//") ? `${issuer}${raw}` : raw);
|
|
3318
|
+
return url.protocol === "https:" || url.protocol === "http:" ? { iconUrl: url.href } : {};
|
|
3319
|
+
} catch {
|
|
3320
|
+
return {};
|
|
3321
|
+
}
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
function legalOf(raw) {
|
|
3325
|
+
const documents = Array.isArray(raw?.documents) ? raw.documents : [];
|
|
3326
|
+
const purposes = Array.isArray(raw?.consent_purposes) ? raw.consent_purposes : [];
|
|
3327
|
+
return {
|
|
3328
|
+
documents: documents.map((d) => ({ kind: String(d.kind ?? ""), version: String(d.version ?? ""), url: String(d.url ?? "") })),
|
|
3329
|
+
consentPurposes: purposes.map((p) => ({ key: String(p.key ?? ""), label: String(p.label ?? ""), required: p.required === true }))
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
function ownNetwork(address) {
|
|
3333
|
+
const a = address.toLowerCase();
|
|
3334
|
+
return /^(127\.|10\.|192\.168\.|169\.254\.|0\.)/.test(a) || /^172\.(1[6-9]|2\d|3[01])\./.test(a) || a === "::1" || /^f[cd]/.test(a) || /^fe[89ab]/.test(a) || a.startsWith("::ffff:127.") || a === "localhost";
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// src/connect.ts
|
|
3338
|
+
function sessionSecret() {
|
|
3339
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
3340
|
+
let s = "";
|
|
3341
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
3342
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3343
|
+
}
|
|
3344
|
+
async function connectSystem(o) {
|
|
3345
|
+
deprecated("connectSystem", "Connect a system with the cauthy CLI: npx cauthy init.");
|
|
3346
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
3347
|
+
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
3348
|
+
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
3349
|
+
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
3350
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
3351
|
+
if (v === "") throw new Error(`@cauthyhq/node: ${name} is required to connect a system`);
|
|
3352
|
+
}
|
|
3353
|
+
const res = await deadline(o.fetch, o.timeoutMs ?? DEFAULT_DEADLINE_MS)(`${issuer}/v1/connect`, {
|
|
3354
|
+
method: "POST",
|
|
3355
|
+
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
|
|
3356
|
+
body: JSON.stringify({
|
|
3357
|
+
system: o.system,
|
|
3358
|
+
app_url: o.appUrl,
|
|
3359
|
+
name: o.name,
|
|
3360
|
+
callback_path: o.callbackPath,
|
|
3361
|
+
events_url: o.eventsUrl,
|
|
3362
|
+
events: o.events,
|
|
3363
|
+
jwks_uri: o.jwksUri
|
|
3364
|
+
})
|
|
3365
|
+
});
|
|
3366
|
+
const body = await res.json().catch(() => null);
|
|
3367
|
+
if (!res.ok || !body?.data) {
|
|
3368
|
+
const why = (body?.errors ?? []).map((e) => e.message).filter(Boolean).join("; ") || res.statusText;
|
|
3369
|
+
throw new Error(`@cauthyhq/node: Cauthy did not connect the system (${res.status}): ${why}`);
|
|
3370
|
+
}
|
|
3371
|
+
const d = body.data;
|
|
3372
|
+
return {
|
|
3373
|
+
system: d.system,
|
|
3374
|
+
clientId: d.client.client_id,
|
|
3375
|
+
clientSecret: d.client_secret,
|
|
3376
|
+
redirectUris: d.client.redirect_uris,
|
|
3377
|
+
webhook: d.webhook,
|
|
3378
|
+
env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
export {
|
|
3382
|
+
CauthyApiError,
|
|
3383
|
+
CauthyConfigError,
|
|
3384
|
+
CauthyTokenError,
|
|
3385
|
+
CauthyUnavailableError,
|
|
3386
|
+
CauthyWebhookError,
|
|
3387
|
+
DEFAULT_PASSWORD_POLICY,
|
|
3388
|
+
MIN_PASSWORD,
|
|
3389
|
+
PROTOCOL_VERSION,
|
|
3390
|
+
STANDARD_VARIABLES,
|
|
3391
|
+
can,
|
|
3392
|
+
checkCode,
|
|
3393
|
+
checkEmail,
|
|
3394
|
+
checkName,
|
|
3395
|
+
checkNewPassword,
|
|
3396
|
+
checkPassword,
|
|
3397
|
+
connectSystem,
|
|
3398
|
+
createCauthy,
|
|
3399
|
+
createSystem,
|
|
3400
|
+
createTokenVerifier,
|
|
3401
|
+
definePolicy,
|
|
3402
|
+
describePasswordPolicy,
|
|
3403
|
+
explainPasswordProblems,
|
|
3404
|
+
failClosed,
|
|
3405
|
+
memoryReplayStore,
|
|
3406
|
+
passwordPolicyOf,
|
|
3407
|
+
passwordProblems,
|
|
3408
|
+
policySource,
|
|
3409
|
+
pullCatalog,
|
|
3410
|
+
pullPolicy,
|
|
3411
|
+
pushCatalog,
|
|
3412
|
+
pushPolicy,
|
|
3413
|
+
pushTheme,
|
|
3414
|
+
readContract,
|
|
3415
|
+
remoteKeys,
|
|
3416
|
+
themeFromStackmyth,
|
|
3417
|
+
tokensFromStackmyth,
|
|
3418
|
+
verifyToken,
|
|
3419
|
+
verifyWebhook
|
|
3420
|
+
};
|
|
3421
|
+
//# sourceMappingURL=index.mjs.map
|