cauthy-auth 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/dist/{cauthy-XkIbG5gE.d.mts → cauthy-BXgsZ-Um.d.mts} +19 -1
- package/dist/{cauthy-DGAy45G2.d.ts → cauthy-CHENs452.d.ts} +19 -1
- package/dist/cli.js +155 -54
- package/dist/client.d.mts +47 -7
- package/dist/client.d.ts +47 -7
- package/dist/client.js +23 -1
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +23 -1
- package/dist/client.mjs.map +1 -1
- package/dist/express.d.mts +2 -2
- package/dist/express.d.ts +2 -2
- package/dist/fastify.d.mts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/hono.d.mts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +352 -131
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +350 -131
- package/dist/index.mjs.map +1 -1
- package/dist/next.d.mts +2 -2
- package/dist/next.d.ts +2 -2
- package/dist/react.d.mts +214 -18
- package/dist/react.d.ts +214 -18
- package/dist/react.js +129 -32
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +129 -32
- package/dist/react.mjs.map +1 -1
- package/dist/svelte.d.mts +2 -2
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +37 -2
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +37 -2
- package/dist/svelte.mjs.map +1 -1
- package/dist/{system-BxMsSIu1.d.mts → system-0mHrdAgO.d.mts} +312 -112
- package/dist/{system-DTIeDXDP.d.ts → system-BIYbS7h2.d.ts} +312 -112
- package/dist/vue.d.mts +2 -2
- package/dist/vue.d.ts +2 -2
- package/dist/vue.js +37 -2
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +37 -2
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,7 +41,9 @@ __export(src_exports, {
|
|
|
41
41
|
passwordPolicyOf: () => passwordPolicyOf,
|
|
42
42
|
passwordProblems: () => passwordProblems,
|
|
43
43
|
policySource: () => policySource,
|
|
44
|
+
pullCatalog: () => pullCatalog,
|
|
44
45
|
pullPolicy: () => pullPolicy,
|
|
46
|
+
pushCatalog: () => pushCatalog,
|
|
45
47
|
pushPolicy: () => pushPolicy,
|
|
46
48
|
pushTheme: () => pushTheme,
|
|
47
49
|
themeFromStackmyth: () => themeFromStackmyth,
|
|
@@ -107,9 +109,6 @@ async function unseal(sealed, key) {
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
// src/system.ts
|
|
111
|
-
var import_jose2 = require("jose");
|
|
112
|
-
|
|
113
112
|
// src/url.ts
|
|
114
113
|
function withoutTrailingSlashes(s) {
|
|
115
114
|
let end = s.length;
|
|
@@ -117,6 +116,218 @@ function withoutTrailingSlashes(s) {
|
|
|
117
116
|
return s.slice(0, end);
|
|
118
117
|
}
|
|
119
118
|
|
|
119
|
+
// src/permissions.ts
|
|
120
|
+
function matches(entry, resource, action) {
|
|
121
|
+
const i = entry.lastIndexOf(":");
|
|
122
|
+
if (i < 0) return false;
|
|
123
|
+
const r = entry.slice(0, i);
|
|
124
|
+
const a = entry.slice(i + 1);
|
|
125
|
+
return (r === "*" || r === resource) && (a === "*" || a === action);
|
|
126
|
+
}
|
|
127
|
+
function can(organization, resource, action) {
|
|
128
|
+
if (!organization?.permissions) return false;
|
|
129
|
+
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
130
|
+
return organization.permissions.some((p) => matches(p, resource, action));
|
|
131
|
+
}
|
|
132
|
+
function definePolicy(given) {
|
|
133
|
+
const roles = Array.isArray(given) ? given : given.roles;
|
|
134
|
+
const resources = Array.isArray(given) ? [] : given.catalog ?? [];
|
|
135
|
+
const entry = (p) => `${p.resource}:${p.action}`;
|
|
136
|
+
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
137
|
+
const split = (permission) => {
|
|
138
|
+
const i = permission.lastIndexOf(":");
|
|
139
|
+
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
roles: roles.map((r) => ({
|
|
143
|
+
role: r.role,
|
|
144
|
+
...r.name !== void 0 ? { name: r.name } : {},
|
|
145
|
+
...r.description !== void 0 ? { description: r.description } : {},
|
|
146
|
+
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
147
|
+
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
148
|
+
})),
|
|
149
|
+
catalog: resources.map((c) => ({
|
|
150
|
+
resource: c.resource,
|
|
151
|
+
...c.description !== void 0 ? { description: c.description } : {},
|
|
152
|
+
actions: [...c.actions],
|
|
153
|
+
...c.attributes ? { attributes: { ...c.attributes } } : {}
|
|
154
|
+
})),
|
|
155
|
+
roleNames: roles.map((r) => r.role),
|
|
156
|
+
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
157
|
+
can(organization, permission) {
|
|
158
|
+
const [resource, action] = split(permission);
|
|
159
|
+
return can(organization, resource, action);
|
|
160
|
+
},
|
|
161
|
+
hasRole(organization, ...wanted) {
|
|
162
|
+
return organization != null && wanted.includes(organization.role);
|
|
163
|
+
},
|
|
164
|
+
permissionsOf(role) {
|
|
165
|
+
const seen = /* @__PURE__ */ new Set();
|
|
166
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
167
|
+
const walk = (name) => {
|
|
168
|
+
if (seen.has(name)) return;
|
|
169
|
+
seen.add(name);
|
|
170
|
+
const r = byName.get(name);
|
|
171
|
+
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
172
|
+
for (const parent of r?.inherits ?? []) walk(parent);
|
|
173
|
+
};
|
|
174
|
+
walk(role);
|
|
175
|
+
return [...out2];
|
|
176
|
+
},
|
|
177
|
+
// The types did the work; this only hands the question on, as a method of
|
|
178
|
+
// the asker so it keeps its `this`.
|
|
179
|
+
check(asker, ...args) {
|
|
180
|
+
return asker.check(...args);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
185
|
+
function policySource(system, read) {
|
|
186
|
+
const roles = Array.isArray(read) ? read : read.roles;
|
|
187
|
+
const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
|
|
188
|
+
const text = (v) => JSON.stringify(v);
|
|
189
|
+
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
190
|
+
const one = (r, pad) => {
|
|
191
|
+
const parts = [`role: ${text(r.role)}`];
|
|
192
|
+
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
193
|
+
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
194
|
+
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
195
|
+
if (r.permissions?.length) {
|
|
196
|
+
const list = r.permissions.map((p) => `${pad} { ${[
|
|
197
|
+
...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [],
|
|
198
|
+
`resource: ${text(p.resource)}`,
|
|
199
|
+
`action: ${text(p.action)}`,
|
|
200
|
+
...p.condition ? [`condition: ${text(p.condition)}`] : []
|
|
201
|
+
].join(", ")} },`);
|
|
202
|
+
parts.push(`permissions: [
|
|
203
|
+
${list.join("\n")}
|
|
204
|
+
${pad} ]`);
|
|
205
|
+
}
|
|
206
|
+
return `${pad} {
|
|
207
|
+
${pad} ${parts.join(`,
|
|
208
|
+
${pad} `)},
|
|
209
|
+
${pad} },`;
|
|
210
|
+
};
|
|
211
|
+
const resource = (c) => {
|
|
212
|
+
const parts = [`resource: ${text(c.resource)}`];
|
|
213
|
+
if (c.description) parts.push(`description: ${text(c.description)}`);
|
|
214
|
+
parts.push(`actions: [${c.actions.map(text).join(", ")}]`);
|
|
215
|
+
const attrs = Object.entries(c.attributes ?? {});
|
|
216
|
+
if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${text(v)}`).join(", ")} }`);
|
|
217
|
+
return ` { ${parts.join(", ")} },`;
|
|
218
|
+
};
|
|
219
|
+
const head = `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
220
|
+
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
221
|
+
// changes, and do not edit this file by hand.
|
|
222
|
+
`;
|
|
223
|
+
if (catalog.length === 0) {
|
|
224
|
+
return `${head}
|
|
225
|
+
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
226
|
+
|
|
227
|
+
export const policy = definePolicy([
|
|
228
|
+
${roles.map((r) => one(r, "")).join("\n")}
|
|
229
|
+
])
|
|
230
|
+
|
|
231
|
+
export const { can, hasRole } = policy
|
|
232
|
+
export type Role = RoleOf<typeof policy>
|
|
233
|
+
export type Permission = PermissionOf<typeof policy>
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
return `${head}
|
|
237
|
+
import { definePolicy, type CheckOf, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
238
|
+
|
|
239
|
+
export const policy = definePolicy({
|
|
240
|
+
catalog: [
|
|
241
|
+
${catalog.map(resource).join("\n")}
|
|
242
|
+
],
|
|
243
|
+
roles: [
|
|
244
|
+
${roles.map((r) => one(r, " ")).join("\n")}
|
|
245
|
+
],
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
export const { can, hasRole } = policy
|
|
249
|
+
export type Role = RoleOf<typeof policy>
|
|
250
|
+
export type Permission = PermissionOf<typeof policy>
|
|
251
|
+
/** What a check may ask: \`policy.check(auth, request, { organization, ...question })\`. */
|
|
252
|
+
export type Check = CheckOf<typeof policy>
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
function reach(o, doing) {
|
|
256
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
257
|
+
const r = {
|
|
258
|
+
issuer: withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? ""),
|
|
259
|
+
system: o.system ?? env4("CAUTHY_SYSTEM") ?? "",
|
|
260
|
+
tenant: o.tenant ?? env4("CAUTHY_TENANT") ?? "",
|
|
261
|
+
token: o.token ?? env4("CAUTHY_TOKEN") ?? ""
|
|
262
|
+
};
|
|
263
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: r.issuer, CAUTHY_SYSTEM: r.system, CAUTHY_TENANT: r.tenant, CAUTHY_TOKEN: r.token })) {
|
|
264
|
+
if (v === "") throw new Error(`cauthy-auth: ${name} is required to ${doing}`);
|
|
265
|
+
}
|
|
266
|
+
return { ...r, fetch: o.fetch ?? fetch };
|
|
267
|
+
}
|
|
268
|
+
async function systemCall(r, method, what, body) {
|
|
269
|
+
const res = await r.fetch(`${r.issuer}/v1/systems/${encodeURIComponent(r.system)}/${what}`, {
|
|
270
|
+
...method === "PUT" ? { method } : {},
|
|
271
|
+
headers: {
|
|
272
|
+
authorization: `Bearer ${r.token}`,
|
|
273
|
+
"x-tenant-key": r.tenant,
|
|
274
|
+
...body === void 0 ? { accept: "application/json" } : { "content-type": "application/json" }
|
|
275
|
+
},
|
|
276
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
277
|
+
});
|
|
278
|
+
const answer = await res.json().catch(() => null);
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
const verb = method === "GET" ? `refused to give the ${what}` : `refused the ${what}`;
|
|
281
|
+
throw new Error(`cauthy-auth: Cauthy ${verb} (${res.status}): ${answer?.errors?.[0]?.message ?? res.statusText}`);
|
|
282
|
+
}
|
|
283
|
+
return answer?.data;
|
|
284
|
+
}
|
|
285
|
+
async function pullPolicy(o = {}) {
|
|
286
|
+
const data = await systemCall(reach(o, "read a policy"), "GET", "policy");
|
|
287
|
+
return data?.roles ?? [];
|
|
288
|
+
}
|
|
289
|
+
async function pullCatalog(o = {}) {
|
|
290
|
+
const data = await systemCall(reach(o, "read a catalog"), "GET", "catalog");
|
|
291
|
+
return data?.resources ?? [];
|
|
292
|
+
}
|
|
293
|
+
async function pushPolicy(o) {
|
|
294
|
+
const r = reach(o, "push a policy");
|
|
295
|
+
if (o.catalog !== void 0) await systemCall(r, "PUT", "catalog", { resources: o.catalog });
|
|
296
|
+
await systemCall(r, "PUT", "policy", { roles: o.roles });
|
|
297
|
+
}
|
|
298
|
+
async function pushCatalog(o) {
|
|
299
|
+
const data = await systemCall(reach(o, "push a catalog"), "PUT", "catalog", { resources: o.resources });
|
|
300
|
+
return data?.resources ?? [];
|
|
301
|
+
}
|
|
302
|
+
function checkWire(q) {
|
|
303
|
+
return {
|
|
304
|
+
resource: q.resource,
|
|
305
|
+
action: q.action,
|
|
306
|
+
...q.resourceId !== void 0 ? { resource_id: q.resourceId } : {},
|
|
307
|
+
...q.ownerId !== void 0 ? { owner_id: q.ownerId } : {},
|
|
308
|
+
...q.attributes !== void 0 ? { attributes: q.attributes } : {}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function checkResultOf(d) {
|
|
312
|
+
const r = d?.rule;
|
|
313
|
+
const rule = r && typeof r === "object" ? {
|
|
314
|
+
source: r.source === "exception" ? "exception" : "role",
|
|
315
|
+
...r.role ? { role: String(r.role) } : {},
|
|
316
|
+
kind: String(r.kind ?? "*"),
|
|
317
|
+
resource: String(r.resource ?? ""),
|
|
318
|
+
action: String(r.action ?? ""),
|
|
319
|
+
effect: r.effect === "allow" ? "allow" : "deny",
|
|
320
|
+
...r.condition ? { condition: String(r.condition) } : {}
|
|
321
|
+
} : void 0;
|
|
322
|
+
return { allowed: d?.allowed === true, reason: String(d?.reason ?? ""), ...rule ? { rule } : {} };
|
|
323
|
+
}
|
|
324
|
+
function organizationIdOf(o) {
|
|
325
|
+
return typeof o === "string" ? o : typeof o?.id === "string" ? o.id : "";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/system.ts
|
|
329
|
+
var import_jose2 = require("jose");
|
|
330
|
+
|
|
120
331
|
// src/theme.ts
|
|
121
332
|
var MAX_RADIUS = 40;
|
|
122
333
|
function withoutComments(css) {
|
|
@@ -534,6 +745,7 @@ function createSystem(given = {}) {
|
|
|
534
745
|
const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
|
|
535
746
|
const envKey = env("CAUTHY_SYSTEM_KEY");
|
|
536
747
|
const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
|
|
748
|
+
const ownSystem = given.system ?? env("CAUTHY_SYSTEM") ?? "";
|
|
537
749
|
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
538
750
|
const tokenEndpoint = `${issuer}/v1/oauth/token`;
|
|
539
751
|
function configured() {
|
|
@@ -651,6 +863,19 @@ function createSystem(given = {}) {
|
|
|
651
863
|
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
652
864
|
}
|
|
653
865
|
},
|
|
866
|
+
catalog: {
|
|
867
|
+
async get(system) {
|
|
868
|
+
return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
|
|
869
|
+
},
|
|
870
|
+
async set(system, resources) {
|
|
871
|
+
return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
async check(q) {
|
|
875
|
+
const system = q.system ?? ownSystem;
|
|
876
|
+
if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
|
|
877
|
+
return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
|
|
878
|
+
},
|
|
654
879
|
async publicJwks() {
|
|
655
880
|
if (!key) return { keys: [] };
|
|
656
881
|
const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
|
|
@@ -798,6 +1023,12 @@ function checkOrganization(value, required = false) {
|
|
|
798
1023
|
if (v.length > MAX_ORGANIZATION) return { field: "organization", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
799
1024
|
return null;
|
|
800
1025
|
}
|
|
1026
|
+
function checkOrganizationName(value) {
|
|
1027
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
1028
|
+
if (v === "") return { field: "name", code: "required", message: "Enter the organization's name." };
|
|
1029
|
+
if (v.length > MAX_ORGANIZATION) return { field: "name", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
801
1032
|
function checkCode(value) {
|
|
802
1033
|
const v = typeof value === "string" ? value.trim() : "";
|
|
803
1034
|
if (v === "") return { field: "code", code: "required", message: "Enter the code." };
|
|
@@ -899,6 +1130,13 @@ function providerIn(segment) {
|
|
|
899
1130
|
function env2(name) {
|
|
900
1131
|
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
901
1132
|
}
|
|
1133
|
+
function hostOf(origin) {
|
|
1134
|
+
try {
|
|
1135
|
+
return new URL(origin).hostname;
|
|
1136
|
+
} catch {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
902
1140
|
function b64url(bytes) {
|
|
903
1141
|
let s = "";
|
|
904
1142
|
for (const b of bytes) s += String.fromCharCode(b);
|
|
@@ -922,6 +1160,9 @@ function redirect(location, cookies = []) {
|
|
|
922
1160
|
for (const c of cookies) h.append("set-cookie", c);
|
|
923
1161
|
return new Response(null, { status: 302, headers: h });
|
|
924
1162
|
}
|
|
1163
|
+
function conditionalOf(c) {
|
|
1164
|
+
return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
|
|
1165
|
+
}
|
|
925
1166
|
function createCauthy(given = {}) {
|
|
926
1167
|
const issuer = withoutTrailingSlashes(given.issuer ?? env2("CAUTHY_ISSUER") ?? "");
|
|
927
1168
|
const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
|
|
@@ -932,6 +1173,10 @@ function createCauthy(given = {}) {
|
|
|
932
1173
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
|
|
933
1174
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
934
1175
|
if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
|
|
1176
|
+
const baseURL = given.baseURL ?? env2("CAUTHY_BASE_URL");
|
|
1177
|
+
if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
|
|
1178
|
+
throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
|
|
1179
|
+
}
|
|
935
1180
|
const basePath = "/" + (given.basePath ?? "/api/auth").replace(/^\/+|\/+$/g, "");
|
|
936
1181
|
const afterSignIn = localPath(given.afterSignIn, "/");
|
|
937
1182
|
const afterSignOut = localPath(given.afterSignOut, "/");
|
|
@@ -946,8 +1191,13 @@ function createCauthy(given = {}) {
|
|
|
946
1191
|
resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
|
|
947
1192
|
verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
|
|
948
1193
|
};
|
|
949
|
-
const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
|
|
950
|
-
const originOf = (request) =>
|
|
1194
|
+
const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, system: system || void 0, fetch: given.fetch });
|
|
1195
|
+
const originOf = (request) => {
|
|
1196
|
+
if (baseURL) return withoutTrailingSlashes(baseURL);
|
|
1197
|
+
const url = new URL(request.url);
|
|
1198
|
+
const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
|
|
1199
|
+
return proto === "https" || proto === "http" ? `${proto}://${url.host}` : url.origin;
|
|
1200
|
+
};
|
|
951
1201
|
const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
|
|
952
1202
|
async function token(form) {
|
|
953
1203
|
const body2 = new URLSearchParams({ client_id: clientId, ...form });
|
|
@@ -1076,13 +1326,35 @@ function createCauthy(given = {}) {
|
|
|
1076
1326
|
role: String(o.role ?? ""),
|
|
1077
1327
|
metadata: o.metadata ?? {},
|
|
1078
1328
|
...o.external_id ? { externalId: String(o.external_id) } : {},
|
|
1079
|
-
...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [] } : {}
|
|
1329
|
+
...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) : [] } : {}
|
|
1080
1330
|
})
|
|
1081
1331
|
);
|
|
1082
1332
|
if (orgCache.size > 5e3) orgCache.clear();
|
|
1083
1333
|
orgCache.set(session.accessToken, { at: Date.now(), value });
|
|
1084
1334
|
return value;
|
|
1085
1335
|
}
|
|
1336
|
+
async function decide(token2, question) {
|
|
1337
|
+
const org = organizationIdOf(question.organization);
|
|
1338
|
+
if (org === "") return { allowed: false, reason: "no organization named" };
|
|
1339
|
+
const res = await doFetch(`${issuer}/v1/oauth/organizations/${encodeURIComponent(org)}/check`, {
|
|
1340
|
+
method: "POST",
|
|
1341
|
+
headers: { authorization: `Bearer ${token2}`, "content-type": "application/json", accept: "application/json" },
|
|
1342
|
+
body: JSON.stringify(checkWire(question))
|
|
1343
|
+
}).catch(() => null);
|
|
1344
|
+
if (res === null) throw new CauthyApiError(503, "unavailable", "Cauthy could not be reached to check a permission");
|
|
1345
|
+
const body2 = await res.json().catch(() => null);
|
|
1346
|
+
if (res.ok) return checkResultOf(body2?.data);
|
|
1347
|
+
const first = body2?.errors?.[0];
|
|
1348
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
1349
|
+
return { allowed: false, reason: first?.message ?? (res.status === 401 ? "not signed in" : res.status === 404 ? "not a member of the organization" : "refused") };
|
|
1350
|
+
}
|
|
1351
|
+
throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused to check a permission (${res.status}): ${first?.message ?? res.statusText}`);
|
|
1352
|
+
}
|
|
1353
|
+
async function check(who, question) {
|
|
1354
|
+
const session = "accessToken" in who ? who : await getSession(who);
|
|
1355
|
+
if (!session) return { allowed: false, reason: "not signed in" };
|
|
1356
|
+
return decide(session.accessToken, question);
|
|
1357
|
+
}
|
|
1086
1358
|
const forgetting = (fn) => async (...args) => {
|
|
1087
1359
|
const out2 = await fn(...args);
|
|
1088
1360
|
orgCache.clear();
|
|
@@ -1215,7 +1487,15 @@ function createCauthy(given = {}) {
|
|
|
1215
1487
|
name: String(d?.name ?? ""),
|
|
1216
1488
|
kind: String(d?.kind ?? ""),
|
|
1217
1489
|
role: String(d?.role ?? ""),
|
|
1218
|
-
|
|
1490
|
+
// A Cauthy from before a power says nothing of it: then it is not given.
|
|
1491
|
+
can: {
|
|
1492
|
+
readMembers: d?.can?.read_members === true,
|
|
1493
|
+
manageMembers: d?.can?.manage_members === true,
|
|
1494
|
+
removeMembers: d?.can?.remove_members === true,
|
|
1495
|
+
update: d?.can?.update === true,
|
|
1496
|
+
archive: d?.can?.archive === true,
|
|
1497
|
+
delete: d?.can?.delete === true
|
|
1498
|
+
}
|
|
1219
1499
|
});
|
|
1220
1500
|
const orgMemberOf = (d) => ({
|
|
1221
1501
|
userId: String(d?.user_id ?? ""),
|
|
@@ -1245,9 +1525,18 @@ function createCauthy(given = {}) {
|
|
|
1245
1525
|
});
|
|
1246
1526
|
const problem = (status2, code, message, field, headers, reasons) => json({ error: { code, message, ...field ? { field } : {}, ...reasons && reasons.length > 0 ? { reasons } : {} } }, status2, headers);
|
|
1247
1527
|
const invalid = (p) => problem(400, p.code, p.message, p.field);
|
|
1528
|
+
let explained = false;
|
|
1248
1529
|
function ownRequest(request) {
|
|
1249
1530
|
const origin = request.headers.get("origin");
|
|
1250
|
-
if (origin !== null)
|
|
1531
|
+
if (origin !== null) {
|
|
1532
|
+
const own = originOf(request);
|
|
1533
|
+
if (origin === own || origin === new URL(request.url).origin) return true;
|
|
1534
|
+
if (!explained && hostOf(origin) === new URL(own).hostname) {
|
|
1535
|
+
explained = true;
|
|
1536
|
+
console.warn(`cauthy-auth: 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.`);
|
|
1537
|
+
}
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1251
1540
|
return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
|
|
1252
1541
|
}
|
|
1253
1542
|
async function body(request) {
|
|
@@ -1399,7 +1688,10 @@ function createCauthy(given = {}) {
|
|
|
1399
1688
|
"/me/organizations/members/add",
|
|
1400
1689
|
"/me/organizations/members/role",
|
|
1401
1690
|
"/me/organizations/members/remove",
|
|
1402
|
-
"/me/organizations/members/invite"
|
|
1691
|
+
"/me/organizations/members/invite",
|
|
1692
|
+
"/me/organizations/rename",
|
|
1693
|
+
"/me/organizations/archive",
|
|
1694
|
+
"/me/organizations/delete"
|
|
1403
1695
|
].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";
|
|
1404
1696
|
if (!own) return null;
|
|
1405
1697
|
if (tenant === "") {
|
|
@@ -1870,6 +2162,23 @@ function createCauthy(given = {}) {
|
|
|
1870
2162
|
if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
|
|
1871
2163
|
return signedIn(request, res.data);
|
|
1872
2164
|
}
|
|
2165
|
+
if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
|
|
2166
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
2167
|
+
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
2168
|
+
const path = `/v1/auth/me/organizations/${org}`;
|
|
2169
|
+
if (route === "/me/organizations/rename") {
|
|
2170
|
+
const bad = checkOrganizationName(b.name);
|
|
2171
|
+
if (bad) return invalid(bad);
|
|
2172
|
+
const res2 = await as(path, { name: String(b.name).trim() }, "PATCH");
|
|
2173
|
+
if (res2.error) return refused(res2, res2.error.code === "validation_error" ? "name" : void 0);
|
|
2174
|
+
orgCache.delete(mine.token);
|
|
2175
|
+
return json({ status: "renamed", organization: seatOf(res2.data) }, 200, mine.headers);
|
|
2176
|
+
}
|
|
2177
|
+
const res = route === "/me/organizations/archive" ? await as(`${path}/archive`) : await as(path, void 0, "DELETE");
|
|
2178
|
+
if (res.error) return refused(res);
|
|
2179
|
+
orgCache.delete(mine.token);
|
|
2180
|
+
return json({ status: route === "/me/organizations/archive" ? "archived" : "deleted", organizationId: org }, 200, mine.headers);
|
|
2181
|
+
}
|
|
1873
2182
|
if (route.startsWith("/me/organizations/members/")) {
|
|
1874
2183
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
1875
2184
|
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
@@ -2015,6 +2324,7 @@ function createCauthy(given = {}) {
|
|
|
2015
2324
|
const get = request.method === "GET" || request.method === "HEAD";
|
|
2016
2325
|
const mine = await ownRoute(request, url, route);
|
|
2017
2326
|
if (mine !== null) return mine;
|
|
2327
|
+
if (route === "/me/organizations/check") return checkRoute(request);
|
|
2018
2328
|
if (route === "/login" && get) return login(request, url);
|
|
2019
2329
|
if (route === "/callback" && get) return callback(request, url);
|
|
2020
2330
|
if (route === "/logout" && (get || request.method === "POST")) return logout(request);
|
|
@@ -2041,6 +2351,36 @@ function createCauthy(given = {}) {
|
|
|
2041
2351
|
}
|
|
2042
2352
|
return new Response("Not found", { status: 404 });
|
|
2043
2353
|
}
|
|
2354
|
+
async function checkRoute(request) {
|
|
2355
|
+
if (request.method !== "POST") return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
2356
|
+
if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
|
|
2357
|
+
const b = await body(request);
|
|
2358
|
+
if (b === null) return problem(400, "bad_request", "Send the question as JSON.");
|
|
2359
|
+
const asker = await signedInAs(request);
|
|
2360
|
+
if ("response" in asker) return asker.response;
|
|
2361
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
2362
|
+
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
|
|
2363
|
+
const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
|
|
2364
|
+
if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
|
|
2365
|
+
const attributes2 = b.attributes;
|
|
2366
|
+
if (attributes2 !== void 0 && (typeof attributes2 !== "object" || attributes2 === null || Array.isArray(attributes2))) {
|
|
2367
|
+
return problem(400, "bad_request", "The attributes are an object.", void 0, asker.headers);
|
|
2368
|
+
}
|
|
2369
|
+
try {
|
|
2370
|
+
const r = await decide(asker.token, {
|
|
2371
|
+
organization: org,
|
|
2372
|
+
resource: b.resource,
|
|
2373
|
+
action: b.action,
|
|
2374
|
+
...attributes2 !== void 0 ? { attributes: attributes2 } : {},
|
|
2375
|
+
...typeof b.resourceId === "string" ? { resourceId: b.resourceId } : {},
|
|
2376
|
+
...typeof b.ownerId === "string" ? { ownerId: b.ownerId } : {}
|
|
2377
|
+
});
|
|
2378
|
+
return json({ status: "decided", ...r }, 200, asker.headers);
|
|
2379
|
+
} catch (e) {
|
|
2380
|
+
const err = e instanceof CauthyApiError ? e : new CauthyApiError(503, "unavailable", "Cauthy could not be reached.");
|
|
2381
|
+
return problem(err.status, err.code, err.message.replace(/^cauthy-auth: /, ""), void 0, asker.headers);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2044
2384
|
async function protect(request) {
|
|
2045
2385
|
const { value } = await read(request);
|
|
2046
2386
|
if (value !== null && fresh(value)) return { session: toSession(value) };
|
|
@@ -2064,6 +2404,7 @@ function createCauthy(given = {}) {
|
|
|
2064
2404
|
resolve: resolve2,
|
|
2065
2405
|
protect,
|
|
2066
2406
|
organizations,
|
|
2407
|
+
check,
|
|
2067
2408
|
toPublic,
|
|
2068
2409
|
urls,
|
|
2069
2410
|
options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
|
|
@@ -2078,128 +2419,6 @@ function legalOf(raw) {
|
|
|
2078
2419
|
};
|
|
2079
2420
|
}
|
|
2080
2421
|
|
|
2081
|
-
// src/permissions.ts
|
|
2082
|
-
function matches(entry, resource, action) {
|
|
2083
|
-
const i = entry.lastIndexOf(":");
|
|
2084
|
-
if (i < 0) return false;
|
|
2085
|
-
const r = entry.slice(0, i);
|
|
2086
|
-
const a = entry.slice(i + 1);
|
|
2087
|
-
return (r === "*" || r === resource) && (a === "*" || a === action);
|
|
2088
|
-
}
|
|
2089
|
-
function can(organization, resource, action) {
|
|
2090
|
-
if (!organization?.permissions) return false;
|
|
2091
|
-
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
2092
|
-
return organization.permissions.some((p) => matches(p, resource, action));
|
|
2093
|
-
}
|
|
2094
|
-
function definePolicy(roles) {
|
|
2095
|
-
const entry = (p) => `${p.resource}:${p.action}`;
|
|
2096
|
-
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
2097
|
-
const split = (permission) => {
|
|
2098
|
-
const i = permission.lastIndexOf(":");
|
|
2099
|
-
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
2100
|
-
};
|
|
2101
|
-
return {
|
|
2102
|
-
roles: roles.map((r) => ({
|
|
2103
|
-
role: r.role,
|
|
2104
|
-
...r.name !== void 0 ? { name: r.name } : {},
|
|
2105
|
-
...r.description !== void 0 ? { description: r.description } : {},
|
|
2106
|
-
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
2107
|
-
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
2108
|
-
})),
|
|
2109
|
-
roleNames: roles.map((r) => r.role),
|
|
2110
|
-
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
2111
|
-
can(organization, permission) {
|
|
2112
|
-
const [resource, action] = split(permission);
|
|
2113
|
-
return can(organization, resource, action);
|
|
2114
|
-
},
|
|
2115
|
-
hasRole(organization, ...wanted) {
|
|
2116
|
-
return organization != null && wanted.includes(organization.role);
|
|
2117
|
-
},
|
|
2118
|
-
permissionsOf(role) {
|
|
2119
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2120
|
-
const out2 = /* @__PURE__ */ new Set();
|
|
2121
|
-
const walk = (name) => {
|
|
2122
|
-
if (seen.has(name)) return;
|
|
2123
|
-
seen.add(name);
|
|
2124
|
-
const r = byName.get(name);
|
|
2125
|
-
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
2126
|
-
for (const parent of r?.inherits ?? []) walk(parent);
|
|
2127
|
-
};
|
|
2128
|
-
walk(role);
|
|
2129
|
-
return [...out2];
|
|
2130
|
-
}
|
|
2131
|
-
};
|
|
2132
|
-
}
|
|
2133
|
-
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2134
|
-
function policySource(system, roles) {
|
|
2135
|
-
const text = (v) => JSON.stringify(v);
|
|
2136
|
-
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
2137
|
-
const one = (r) => {
|
|
2138
|
-
const parts = [`role: ${text(r.role)}`];
|
|
2139
|
-
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
2140
|
-
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
2141
|
-
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
2142
|
-
if (r.permissions?.length) {
|
|
2143
|
-
const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
|
|
2144
|
-
parts.push(`permissions: [
|
|
2145
|
-
${list.join("\n")}
|
|
2146
|
-
]`);
|
|
2147
|
-
}
|
|
2148
|
-
return ` {
|
|
2149
|
-
${parts.join(",\n ")},
|
|
2150
|
-
},`;
|
|
2151
|
-
};
|
|
2152
|
-
return `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
2153
|
-
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
2154
|
-
// changes, and do not edit this file by hand.
|
|
2155
|
-
|
|
2156
|
-
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
2157
|
-
|
|
2158
|
-
export const policy = definePolicy([
|
|
2159
|
-
${roles.map(one).join("\n")}
|
|
2160
|
-
])
|
|
2161
|
-
|
|
2162
|
-
export const { can, hasRole } = policy
|
|
2163
|
-
export type Role = RoleOf<typeof policy>
|
|
2164
|
-
export type Permission = PermissionOf<typeof policy>
|
|
2165
|
-
`;
|
|
2166
|
-
}
|
|
2167
|
-
async function pullPolicy(o = {}) {
|
|
2168
|
-
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
2169
|
-
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
2170
|
-
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
2171
|
-
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
2172
|
-
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
2173
|
-
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
2174
|
-
if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
|
|
2175
|
-
}
|
|
2176
|
-
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
2177
|
-
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
|
|
2178
|
-
});
|
|
2179
|
-
const body = await res.json().catch(() => null);
|
|
2180
|
-
if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
2181
|
-
return body?.data?.roles ?? [];
|
|
2182
|
-
}
|
|
2183
|
-
async function pushPolicy(o) {
|
|
2184
|
-
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
2185
|
-
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
2186
|
-
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
2187
|
-
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
2188
|
-
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
2189
|
-
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
2190
|
-
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
|
|
2191
|
-
}
|
|
2192
|
-
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
2193
|
-
method: "PUT",
|
|
2194
|
-
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
|
|
2195
|
-
body: JSON.stringify({ roles: o.roles })
|
|
2196
|
-
});
|
|
2197
|
-
if (!res.ok) {
|
|
2198
|
-
const body = await res.json().catch(() => null);
|
|
2199
|
-
throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
2422
|
// src/connect.ts
|
|
2204
2423
|
function sessionSecret() {
|
|
2205
2424
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -2325,7 +2544,9 @@ async function verifyToken(from, options = {}) {
|
|
|
2325
2544
|
passwordPolicyOf,
|
|
2326
2545
|
passwordProblems,
|
|
2327
2546
|
policySource,
|
|
2547
|
+
pullCatalog,
|
|
2328
2548
|
pullPolicy,
|
|
2549
|
+
pushCatalog,
|
|
2329
2550
|
pushPolicy,
|
|
2330
2551
|
pushTheme,
|
|
2331
2552
|
themeFromStackmyth,
|