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.mjs
CHANGED
|
@@ -54,9 +54,6 @@ async function unseal(sealed, key) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// src/system.ts
|
|
58
|
-
import { exportJWK, importJWK, importPKCS8, SignJWT } from "jose";
|
|
59
|
-
|
|
60
57
|
// src/url.ts
|
|
61
58
|
function withoutTrailingSlashes(s) {
|
|
62
59
|
let end = s.length;
|
|
@@ -64,6 +61,218 @@ function withoutTrailingSlashes(s) {
|
|
|
64
61
|
return s.slice(0, end);
|
|
65
62
|
}
|
|
66
63
|
|
|
64
|
+
// src/permissions.ts
|
|
65
|
+
function matches(entry, resource, action) {
|
|
66
|
+
const i = entry.lastIndexOf(":");
|
|
67
|
+
if (i < 0) return false;
|
|
68
|
+
const r = entry.slice(0, i);
|
|
69
|
+
const a = entry.slice(i + 1);
|
|
70
|
+
return (r === "*" || r === resource) && (a === "*" || a === action);
|
|
71
|
+
}
|
|
72
|
+
function can(organization, resource, action) {
|
|
73
|
+
if (!organization?.permissions) return false;
|
|
74
|
+
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
75
|
+
return organization.permissions.some((p) => matches(p, resource, action));
|
|
76
|
+
}
|
|
77
|
+
function definePolicy(given) {
|
|
78
|
+
const roles = Array.isArray(given) ? given : given.roles;
|
|
79
|
+
const resources = Array.isArray(given) ? [] : given.catalog ?? [];
|
|
80
|
+
const entry = (p) => `${p.resource}:${p.action}`;
|
|
81
|
+
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
82
|
+
const split = (permission) => {
|
|
83
|
+
const i = permission.lastIndexOf(":");
|
|
84
|
+
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
roles: roles.map((r) => ({
|
|
88
|
+
role: r.role,
|
|
89
|
+
...r.name !== void 0 ? { name: r.name } : {},
|
|
90
|
+
...r.description !== void 0 ? { description: r.description } : {},
|
|
91
|
+
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
92
|
+
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
93
|
+
})),
|
|
94
|
+
catalog: resources.map((c) => ({
|
|
95
|
+
resource: c.resource,
|
|
96
|
+
...c.description !== void 0 ? { description: c.description } : {},
|
|
97
|
+
actions: [...c.actions],
|
|
98
|
+
...c.attributes ? { attributes: { ...c.attributes } } : {}
|
|
99
|
+
})),
|
|
100
|
+
roleNames: roles.map((r) => r.role),
|
|
101
|
+
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
102
|
+
can(organization, permission) {
|
|
103
|
+
const [resource, action] = split(permission);
|
|
104
|
+
return can(organization, resource, action);
|
|
105
|
+
},
|
|
106
|
+
hasRole(organization, ...wanted) {
|
|
107
|
+
return organization != null && wanted.includes(organization.role);
|
|
108
|
+
},
|
|
109
|
+
permissionsOf(role) {
|
|
110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
111
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
112
|
+
const walk = (name) => {
|
|
113
|
+
if (seen.has(name)) return;
|
|
114
|
+
seen.add(name);
|
|
115
|
+
const r = byName.get(name);
|
|
116
|
+
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
117
|
+
for (const parent of r?.inherits ?? []) walk(parent);
|
|
118
|
+
};
|
|
119
|
+
walk(role);
|
|
120
|
+
return [...out2];
|
|
121
|
+
},
|
|
122
|
+
// The types did the work; this only hands the question on, as a method of
|
|
123
|
+
// the asker so it keeps its `this`.
|
|
124
|
+
check(asker, ...args) {
|
|
125
|
+
return asker.check(...args);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
130
|
+
function policySource(system, read) {
|
|
131
|
+
const roles = Array.isArray(read) ? read : read.roles;
|
|
132
|
+
const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
|
|
133
|
+
const text = (v) => JSON.stringify(v);
|
|
134
|
+
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
135
|
+
const one = (r, pad) => {
|
|
136
|
+
const parts = [`role: ${text(r.role)}`];
|
|
137
|
+
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
138
|
+
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
139
|
+
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
140
|
+
if (r.permissions?.length) {
|
|
141
|
+
const list = r.permissions.map((p) => `${pad} { ${[
|
|
142
|
+
...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [],
|
|
143
|
+
`resource: ${text(p.resource)}`,
|
|
144
|
+
`action: ${text(p.action)}`,
|
|
145
|
+
...p.condition ? [`condition: ${text(p.condition)}`] : []
|
|
146
|
+
].join(", ")} },`);
|
|
147
|
+
parts.push(`permissions: [
|
|
148
|
+
${list.join("\n")}
|
|
149
|
+
${pad} ]`);
|
|
150
|
+
}
|
|
151
|
+
return `${pad} {
|
|
152
|
+
${pad} ${parts.join(`,
|
|
153
|
+
${pad} `)},
|
|
154
|
+
${pad} },`;
|
|
155
|
+
};
|
|
156
|
+
const resource = (c) => {
|
|
157
|
+
const parts = [`resource: ${text(c.resource)}`];
|
|
158
|
+
if (c.description) parts.push(`description: ${text(c.description)}`);
|
|
159
|
+
parts.push(`actions: [${c.actions.map(text).join(", ")}]`);
|
|
160
|
+
const attrs = Object.entries(c.attributes ?? {});
|
|
161
|
+
if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${text(v)}`).join(", ")} }`);
|
|
162
|
+
return ` { ${parts.join(", ")} },`;
|
|
163
|
+
};
|
|
164
|
+
const head = `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
165
|
+
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
166
|
+
// changes, and do not edit this file by hand.
|
|
167
|
+
`;
|
|
168
|
+
if (catalog.length === 0) {
|
|
169
|
+
return `${head}
|
|
170
|
+
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
171
|
+
|
|
172
|
+
export const policy = definePolicy([
|
|
173
|
+
${roles.map((r) => one(r, "")).join("\n")}
|
|
174
|
+
])
|
|
175
|
+
|
|
176
|
+
export const { can, hasRole } = policy
|
|
177
|
+
export type Role = RoleOf<typeof policy>
|
|
178
|
+
export type Permission = PermissionOf<typeof policy>
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
return `${head}
|
|
182
|
+
import { definePolicy, type CheckOf, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
183
|
+
|
|
184
|
+
export const policy = definePolicy({
|
|
185
|
+
catalog: [
|
|
186
|
+
${catalog.map(resource).join("\n")}
|
|
187
|
+
],
|
|
188
|
+
roles: [
|
|
189
|
+
${roles.map((r) => one(r, " ")).join("\n")}
|
|
190
|
+
],
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
export const { can, hasRole } = policy
|
|
194
|
+
export type Role = RoleOf<typeof policy>
|
|
195
|
+
export type Permission = PermissionOf<typeof policy>
|
|
196
|
+
/** What a check may ask: \`policy.check(auth, request, { organization, ...question })\`. */
|
|
197
|
+
export type Check = CheckOf<typeof policy>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
function reach(o, doing) {
|
|
201
|
+
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
202
|
+
const r = {
|
|
203
|
+
issuer: withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? ""),
|
|
204
|
+
system: o.system ?? env4("CAUTHY_SYSTEM") ?? "",
|
|
205
|
+
tenant: o.tenant ?? env4("CAUTHY_TENANT") ?? "",
|
|
206
|
+
token: o.token ?? env4("CAUTHY_TOKEN") ?? ""
|
|
207
|
+
};
|
|
208
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: r.issuer, CAUTHY_SYSTEM: r.system, CAUTHY_TENANT: r.tenant, CAUTHY_TOKEN: r.token })) {
|
|
209
|
+
if (v === "") throw new Error(`cauthy-auth: ${name} is required to ${doing}`);
|
|
210
|
+
}
|
|
211
|
+
return { ...r, fetch: o.fetch ?? fetch };
|
|
212
|
+
}
|
|
213
|
+
async function systemCall(r, method, what, body) {
|
|
214
|
+
const res = await r.fetch(`${r.issuer}/v1/systems/${encodeURIComponent(r.system)}/${what}`, {
|
|
215
|
+
...method === "PUT" ? { method } : {},
|
|
216
|
+
headers: {
|
|
217
|
+
authorization: `Bearer ${r.token}`,
|
|
218
|
+
"x-tenant-key": r.tenant,
|
|
219
|
+
...body === void 0 ? { accept: "application/json" } : { "content-type": "application/json" }
|
|
220
|
+
},
|
|
221
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
222
|
+
});
|
|
223
|
+
const answer = await res.json().catch(() => null);
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
const verb = method === "GET" ? `refused to give the ${what}` : `refused the ${what}`;
|
|
226
|
+
throw new Error(`cauthy-auth: Cauthy ${verb} (${res.status}): ${answer?.errors?.[0]?.message ?? res.statusText}`);
|
|
227
|
+
}
|
|
228
|
+
return answer?.data;
|
|
229
|
+
}
|
|
230
|
+
async function pullPolicy(o = {}) {
|
|
231
|
+
const data = await systemCall(reach(o, "read a policy"), "GET", "policy");
|
|
232
|
+
return data?.roles ?? [];
|
|
233
|
+
}
|
|
234
|
+
async function pullCatalog(o = {}) {
|
|
235
|
+
const data = await systemCall(reach(o, "read a catalog"), "GET", "catalog");
|
|
236
|
+
return data?.resources ?? [];
|
|
237
|
+
}
|
|
238
|
+
async function pushPolicy(o) {
|
|
239
|
+
const r = reach(o, "push a policy");
|
|
240
|
+
if (o.catalog !== void 0) await systemCall(r, "PUT", "catalog", { resources: o.catalog });
|
|
241
|
+
await systemCall(r, "PUT", "policy", { roles: o.roles });
|
|
242
|
+
}
|
|
243
|
+
async function pushCatalog(o) {
|
|
244
|
+
const data = await systemCall(reach(o, "push a catalog"), "PUT", "catalog", { resources: o.resources });
|
|
245
|
+
return data?.resources ?? [];
|
|
246
|
+
}
|
|
247
|
+
function checkWire(q) {
|
|
248
|
+
return {
|
|
249
|
+
resource: q.resource,
|
|
250
|
+
action: q.action,
|
|
251
|
+
...q.resourceId !== void 0 ? { resource_id: q.resourceId } : {},
|
|
252
|
+
...q.ownerId !== void 0 ? { owner_id: q.ownerId } : {},
|
|
253
|
+
...q.attributes !== void 0 ? { attributes: q.attributes } : {}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function checkResultOf(d) {
|
|
257
|
+
const r = d?.rule;
|
|
258
|
+
const rule = r && typeof r === "object" ? {
|
|
259
|
+
source: r.source === "exception" ? "exception" : "role",
|
|
260
|
+
...r.role ? { role: String(r.role) } : {},
|
|
261
|
+
kind: String(r.kind ?? "*"),
|
|
262
|
+
resource: String(r.resource ?? ""),
|
|
263
|
+
action: String(r.action ?? ""),
|
|
264
|
+
effect: r.effect === "allow" ? "allow" : "deny",
|
|
265
|
+
...r.condition ? { condition: String(r.condition) } : {}
|
|
266
|
+
} : void 0;
|
|
267
|
+
return { allowed: d?.allowed === true, reason: String(d?.reason ?? ""), ...rule ? { rule } : {} };
|
|
268
|
+
}
|
|
269
|
+
function organizationIdOf(o) {
|
|
270
|
+
return typeof o === "string" ? o : typeof o?.id === "string" ? o.id : "";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/system.ts
|
|
274
|
+
import { exportJWK, importJWK, importPKCS8, SignJWT } from "jose";
|
|
275
|
+
|
|
67
276
|
// src/theme.ts
|
|
68
277
|
var MAX_RADIUS = 40;
|
|
69
278
|
function withoutComments(css) {
|
|
@@ -481,6 +690,7 @@ function createSystem(given = {}) {
|
|
|
481
690
|
const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
|
|
482
691
|
const envKey = env("CAUTHY_SYSTEM_KEY");
|
|
483
692
|
const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
|
|
693
|
+
const ownSystem = given.system ?? env("CAUTHY_SYSTEM") ?? "";
|
|
484
694
|
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
485
695
|
const tokenEndpoint = `${issuer}/v1/oauth/token`;
|
|
486
696
|
function configured() {
|
|
@@ -598,6 +808,19 @@ function createSystem(given = {}) {
|
|
|
598
808
|
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
599
809
|
}
|
|
600
810
|
},
|
|
811
|
+
catalog: {
|
|
812
|
+
async get(system) {
|
|
813
|
+
return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
|
|
814
|
+
},
|
|
815
|
+
async set(system, resources) {
|
|
816
|
+
return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
async check(q) {
|
|
820
|
+
const system = q.system ?? ownSystem;
|
|
821
|
+
if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
|
|
822
|
+
return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
|
|
823
|
+
},
|
|
601
824
|
async publicJwks() {
|
|
602
825
|
if (!key) return { keys: [] };
|
|
603
826
|
const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
|
|
@@ -745,6 +968,12 @@ function checkOrganization(value, required = false) {
|
|
|
745
968
|
if (v.length > MAX_ORGANIZATION) return { field: "organization", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
746
969
|
return null;
|
|
747
970
|
}
|
|
971
|
+
function checkOrganizationName(value) {
|
|
972
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
973
|
+
if (v === "") return { field: "name", code: "required", message: "Enter the organization's name." };
|
|
974
|
+
if (v.length > MAX_ORGANIZATION) return { field: "name", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
748
977
|
function checkCode(value) {
|
|
749
978
|
const v = typeof value === "string" ? value.trim() : "";
|
|
750
979
|
if (v === "") return { field: "code", code: "required", message: "Enter the code." };
|
|
@@ -846,6 +1075,13 @@ function providerIn(segment) {
|
|
|
846
1075
|
function env2(name) {
|
|
847
1076
|
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
848
1077
|
}
|
|
1078
|
+
function hostOf(origin) {
|
|
1079
|
+
try {
|
|
1080
|
+
return new URL(origin).hostname;
|
|
1081
|
+
} catch {
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
849
1085
|
function b64url(bytes) {
|
|
850
1086
|
let s = "";
|
|
851
1087
|
for (const b of bytes) s += String.fromCharCode(b);
|
|
@@ -869,6 +1105,9 @@ function redirect(location, cookies = []) {
|
|
|
869
1105
|
for (const c of cookies) h.append("set-cookie", c);
|
|
870
1106
|
return new Response(null, { status: 302, headers: h });
|
|
871
1107
|
}
|
|
1108
|
+
function conditionalOf(c) {
|
|
1109
|
+
return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
|
|
1110
|
+
}
|
|
872
1111
|
function createCauthy(given = {}) {
|
|
873
1112
|
const issuer = withoutTrailingSlashes(given.issuer ?? env2("CAUTHY_ISSUER") ?? "");
|
|
874
1113
|
const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
|
|
@@ -879,6 +1118,10 @@ function createCauthy(given = {}) {
|
|
|
879
1118
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
|
|
880
1119
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
881
1120
|
if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
|
|
1121
|
+
const baseURL = given.baseURL ?? env2("CAUTHY_BASE_URL");
|
|
1122
|
+
if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
|
|
1123
|
+
throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
|
|
1124
|
+
}
|
|
882
1125
|
const basePath = "/" + (given.basePath ?? "/api/auth").replace(/^\/+|\/+$/g, "");
|
|
883
1126
|
const afterSignIn = localPath(given.afterSignIn, "/");
|
|
884
1127
|
const afterSignOut = localPath(given.afterSignOut, "/");
|
|
@@ -893,8 +1136,13 @@ function createCauthy(given = {}) {
|
|
|
893
1136
|
resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
|
|
894
1137
|
verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
|
|
895
1138
|
};
|
|
896
|
-
const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
|
|
897
|
-
const originOf = (request) =>
|
|
1139
|
+
const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, system: system || void 0, fetch: given.fetch });
|
|
1140
|
+
const originOf = (request) => {
|
|
1141
|
+
if (baseURL) return withoutTrailingSlashes(baseURL);
|
|
1142
|
+
const url = new URL(request.url);
|
|
1143
|
+
const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
|
|
1144
|
+
return proto === "https" || proto === "http" ? `${proto}://${url.host}` : url.origin;
|
|
1145
|
+
};
|
|
898
1146
|
const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
|
|
899
1147
|
async function token(form) {
|
|
900
1148
|
const body2 = new URLSearchParams({ client_id: clientId, ...form });
|
|
@@ -1023,13 +1271,35 @@ function createCauthy(given = {}) {
|
|
|
1023
1271
|
role: String(o.role ?? ""),
|
|
1024
1272
|
metadata: o.metadata ?? {},
|
|
1025
1273
|
...o.external_id ? { externalId: String(o.external_id) } : {},
|
|
1026
|
-
...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [] } : {}
|
|
1274
|
+
...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) : [] } : {}
|
|
1027
1275
|
})
|
|
1028
1276
|
);
|
|
1029
1277
|
if (orgCache.size > 5e3) orgCache.clear();
|
|
1030
1278
|
orgCache.set(session.accessToken, { at: Date.now(), value });
|
|
1031
1279
|
return value;
|
|
1032
1280
|
}
|
|
1281
|
+
async function decide(token2, question) {
|
|
1282
|
+
const org = organizationIdOf(question.organization);
|
|
1283
|
+
if (org === "") return { allowed: false, reason: "no organization named" };
|
|
1284
|
+
const res = await doFetch(`${issuer}/v1/oauth/organizations/${encodeURIComponent(org)}/check`, {
|
|
1285
|
+
method: "POST",
|
|
1286
|
+
headers: { authorization: `Bearer ${token2}`, "content-type": "application/json", accept: "application/json" },
|
|
1287
|
+
body: JSON.stringify(checkWire(question))
|
|
1288
|
+
}).catch(() => null);
|
|
1289
|
+
if (res === null) throw new CauthyApiError(503, "unavailable", "Cauthy could not be reached to check a permission");
|
|
1290
|
+
const body2 = await res.json().catch(() => null);
|
|
1291
|
+
if (res.ok) return checkResultOf(body2?.data);
|
|
1292
|
+
const first = body2?.errors?.[0];
|
|
1293
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
1294
|
+
return { allowed: false, reason: first?.message ?? (res.status === 401 ? "not signed in" : res.status === 404 ? "not a member of the organization" : "refused") };
|
|
1295
|
+
}
|
|
1296
|
+
throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused to check a permission (${res.status}): ${first?.message ?? res.statusText}`);
|
|
1297
|
+
}
|
|
1298
|
+
async function check(who, question) {
|
|
1299
|
+
const session = "accessToken" in who ? who : await getSession(who);
|
|
1300
|
+
if (!session) return { allowed: false, reason: "not signed in" };
|
|
1301
|
+
return decide(session.accessToken, question);
|
|
1302
|
+
}
|
|
1033
1303
|
const forgetting = (fn) => async (...args) => {
|
|
1034
1304
|
const out2 = await fn(...args);
|
|
1035
1305
|
orgCache.clear();
|
|
@@ -1162,7 +1432,15 @@ function createCauthy(given = {}) {
|
|
|
1162
1432
|
name: String(d?.name ?? ""),
|
|
1163
1433
|
kind: String(d?.kind ?? ""),
|
|
1164
1434
|
role: String(d?.role ?? ""),
|
|
1165
|
-
|
|
1435
|
+
// A Cauthy from before a power says nothing of it: then it is not given.
|
|
1436
|
+
can: {
|
|
1437
|
+
readMembers: d?.can?.read_members === true,
|
|
1438
|
+
manageMembers: d?.can?.manage_members === true,
|
|
1439
|
+
removeMembers: d?.can?.remove_members === true,
|
|
1440
|
+
update: d?.can?.update === true,
|
|
1441
|
+
archive: d?.can?.archive === true,
|
|
1442
|
+
delete: d?.can?.delete === true
|
|
1443
|
+
}
|
|
1166
1444
|
});
|
|
1167
1445
|
const orgMemberOf = (d) => ({
|
|
1168
1446
|
userId: String(d?.user_id ?? ""),
|
|
@@ -1192,9 +1470,18 @@ function createCauthy(given = {}) {
|
|
|
1192
1470
|
});
|
|
1193
1471
|
const problem = (status2, code, message, field, headers, reasons) => json({ error: { code, message, ...field ? { field } : {}, ...reasons && reasons.length > 0 ? { reasons } : {} } }, status2, headers);
|
|
1194
1472
|
const invalid = (p) => problem(400, p.code, p.message, p.field);
|
|
1473
|
+
let explained = false;
|
|
1195
1474
|
function ownRequest(request) {
|
|
1196
1475
|
const origin = request.headers.get("origin");
|
|
1197
|
-
if (origin !== null)
|
|
1476
|
+
if (origin !== null) {
|
|
1477
|
+
const own = originOf(request);
|
|
1478
|
+
if (origin === own || origin === new URL(request.url).origin) return true;
|
|
1479
|
+
if (!explained && hostOf(origin) === new URL(own).hostname) {
|
|
1480
|
+
explained = true;
|
|
1481
|
+
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.`);
|
|
1482
|
+
}
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1198
1485
|
return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
|
|
1199
1486
|
}
|
|
1200
1487
|
async function body(request) {
|
|
@@ -1346,7 +1633,10 @@ function createCauthy(given = {}) {
|
|
|
1346
1633
|
"/me/organizations/members/add",
|
|
1347
1634
|
"/me/organizations/members/role",
|
|
1348
1635
|
"/me/organizations/members/remove",
|
|
1349
|
-
"/me/organizations/members/invite"
|
|
1636
|
+
"/me/organizations/members/invite",
|
|
1637
|
+
"/me/organizations/rename",
|
|
1638
|
+
"/me/organizations/archive",
|
|
1639
|
+
"/me/organizations/delete"
|
|
1350
1640
|
].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";
|
|
1351
1641
|
if (!own) return null;
|
|
1352
1642
|
if (tenant === "") {
|
|
@@ -1817,6 +2107,23 @@ function createCauthy(given = {}) {
|
|
|
1817
2107
|
if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
|
|
1818
2108
|
return signedIn(request, res.data);
|
|
1819
2109
|
}
|
|
2110
|
+
if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
|
|
2111
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
2112
|
+
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
2113
|
+
const path = `/v1/auth/me/organizations/${org}`;
|
|
2114
|
+
if (route === "/me/organizations/rename") {
|
|
2115
|
+
const bad = checkOrganizationName(b.name);
|
|
2116
|
+
if (bad) return invalid(bad);
|
|
2117
|
+
const res2 = await as(path, { name: String(b.name).trim() }, "PATCH");
|
|
2118
|
+
if (res2.error) return refused(res2, res2.error.code === "validation_error" ? "name" : void 0);
|
|
2119
|
+
orgCache.delete(mine.token);
|
|
2120
|
+
return json({ status: "renamed", organization: seatOf(res2.data) }, 200, mine.headers);
|
|
2121
|
+
}
|
|
2122
|
+
const res = route === "/me/organizations/archive" ? await as(`${path}/archive`) : await as(path, void 0, "DELETE");
|
|
2123
|
+
if (res.error) return refused(res);
|
|
2124
|
+
orgCache.delete(mine.token);
|
|
2125
|
+
return json({ status: route === "/me/organizations/archive" ? "archived" : "deleted", organizationId: org }, 200, mine.headers);
|
|
2126
|
+
}
|
|
1820
2127
|
if (route.startsWith("/me/organizations/members/")) {
|
|
1821
2128
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
1822
2129
|
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
@@ -1962,6 +2269,7 @@ function createCauthy(given = {}) {
|
|
|
1962
2269
|
const get = request.method === "GET" || request.method === "HEAD";
|
|
1963
2270
|
const mine = await ownRoute(request, url, route);
|
|
1964
2271
|
if (mine !== null) return mine;
|
|
2272
|
+
if (route === "/me/organizations/check") return checkRoute(request);
|
|
1965
2273
|
if (route === "/login" && get) return login(request, url);
|
|
1966
2274
|
if (route === "/callback" && get) return callback(request, url);
|
|
1967
2275
|
if (route === "/logout" && (get || request.method === "POST")) return logout(request);
|
|
@@ -1988,6 +2296,36 @@ function createCauthy(given = {}) {
|
|
|
1988
2296
|
}
|
|
1989
2297
|
return new Response("Not found", { status: 404 });
|
|
1990
2298
|
}
|
|
2299
|
+
async function checkRoute(request) {
|
|
2300
|
+
if (request.method !== "POST") return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
2301
|
+
if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
|
|
2302
|
+
const b = await body(request);
|
|
2303
|
+
if (b === null) return problem(400, "bad_request", "Send the question as JSON.");
|
|
2304
|
+
const asker = await signedInAs(request);
|
|
2305
|
+
if ("response" in asker) return asker.response;
|
|
2306
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
2307
|
+
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
|
|
2308
|
+
const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
|
|
2309
|
+
if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
|
|
2310
|
+
const attributes2 = b.attributes;
|
|
2311
|
+
if (attributes2 !== void 0 && (typeof attributes2 !== "object" || attributes2 === null || Array.isArray(attributes2))) {
|
|
2312
|
+
return problem(400, "bad_request", "The attributes are an object.", void 0, asker.headers);
|
|
2313
|
+
}
|
|
2314
|
+
try {
|
|
2315
|
+
const r = await decide(asker.token, {
|
|
2316
|
+
organization: org,
|
|
2317
|
+
resource: b.resource,
|
|
2318
|
+
action: b.action,
|
|
2319
|
+
...attributes2 !== void 0 ? { attributes: attributes2 } : {},
|
|
2320
|
+
...typeof b.resourceId === "string" ? { resourceId: b.resourceId } : {},
|
|
2321
|
+
...typeof b.ownerId === "string" ? { ownerId: b.ownerId } : {}
|
|
2322
|
+
});
|
|
2323
|
+
return json({ status: "decided", ...r }, 200, asker.headers);
|
|
2324
|
+
} catch (e) {
|
|
2325
|
+
const err = e instanceof CauthyApiError ? e : new CauthyApiError(503, "unavailable", "Cauthy could not be reached.");
|
|
2326
|
+
return problem(err.status, err.code, err.message.replace(/^cauthy-auth: /, ""), void 0, asker.headers);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
1991
2329
|
async function protect(request) {
|
|
1992
2330
|
const { value } = await read(request);
|
|
1993
2331
|
if (value !== null && fresh(value)) return { session: toSession(value) };
|
|
@@ -2011,6 +2349,7 @@ function createCauthy(given = {}) {
|
|
|
2011
2349
|
resolve: resolve2,
|
|
2012
2350
|
protect,
|
|
2013
2351
|
organizations,
|
|
2352
|
+
check,
|
|
2014
2353
|
toPublic,
|
|
2015
2354
|
urls,
|
|
2016
2355
|
options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
|
|
@@ -2025,128 +2364,6 @@ function legalOf(raw) {
|
|
|
2025
2364
|
};
|
|
2026
2365
|
}
|
|
2027
2366
|
|
|
2028
|
-
// src/permissions.ts
|
|
2029
|
-
function matches(entry, resource, action) {
|
|
2030
|
-
const i = entry.lastIndexOf(":");
|
|
2031
|
-
if (i < 0) return false;
|
|
2032
|
-
const r = entry.slice(0, i);
|
|
2033
|
-
const a = entry.slice(i + 1);
|
|
2034
|
-
return (r === "*" || r === resource) && (a === "*" || a === action);
|
|
2035
|
-
}
|
|
2036
|
-
function can(organization, resource, action) {
|
|
2037
|
-
if (!organization?.permissions) return false;
|
|
2038
|
-
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
2039
|
-
return organization.permissions.some((p) => matches(p, resource, action));
|
|
2040
|
-
}
|
|
2041
|
-
function definePolicy(roles) {
|
|
2042
|
-
const entry = (p) => `${p.resource}:${p.action}`;
|
|
2043
|
-
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
2044
|
-
const split = (permission) => {
|
|
2045
|
-
const i = permission.lastIndexOf(":");
|
|
2046
|
-
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
2047
|
-
};
|
|
2048
|
-
return {
|
|
2049
|
-
roles: roles.map((r) => ({
|
|
2050
|
-
role: r.role,
|
|
2051
|
-
...r.name !== void 0 ? { name: r.name } : {},
|
|
2052
|
-
...r.description !== void 0 ? { description: r.description } : {},
|
|
2053
|
-
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
2054
|
-
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
2055
|
-
})),
|
|
2056
|
-
roleNames: roles.map((r) => r.role),
|
|
2057
|
-
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
2058
|
-
can(organization, permission) {
|
|
2059
|
-
const [resource, action] = split(permission);
|
|
2060
|
-
return can(organization, resource, action);
|
|
2061
|
-
},
|
|
2062
|
-
hasRole(organization, ...wanted) {
|
|
2063
|
-
return organization != null && wanted.includes(organization.role);
|
|
2064
|
-
},
|
|
2065
|
-
permissionsOf(role) {
|
|
2066
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2067
|
-
const out2 = /* @__PURE__ */ new Set();
|
|
2068
|
-
const walk = (name) => {
|
|
2069
|
-
if (seen.has(name)) return;
|
|
2070
|
-
seen.add(name);
|
|
2071
|
-
const r = byName.get(name);
|
|
2072
|
-
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
2073
|
-
for (const parent of r?.inherits ?? []) walk(parent);
|
|
2074
|
-
};
|
|
2075
|
-
walk(role);
|
|
2076
|
-
return [...out2];
|
|
2077
|
-
}
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2081
|
-
function policySource(system, roles) {
|
|
2082
|
-
const text = (v) => JSON.stringify(v);
|
|
2083
|
-
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
2084
|
-
const one = (r) => {
|
|
2085
|
-
const parts = [`role: ${text(r.role)}`];
|
|
2086
|
-
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
2087
|
-
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
2088
|
-
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
2089
|
-
if (r.permissions?.length) {
|
|
2090
|
-
const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
|
|
2091
|
-
parts.push(`permissions: [
|
|
2092
|
-
${list.join("\n")}
|
|
2093
|
-
]`);
|
|
2094
|
-
}
|
|
2095
|
-
return ` {
|
|
2096
|
-
${parts.join(",\n ")},
|
|
2097
|
-
},`;
|
|
2098
|
-
};
|
|
2099
|
-
return `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
2100
|
-
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
2101
|
-
// changes, and do not edit this file by hand.
|
|
2102
|
-
|
|
2103
|
-
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
2104
|
-
|
|
2105
|
-
export const policy = definePolicy([
|
|
2106
|
-
${roles.map(one).join("\n")}
|
|
2107
|
-
])
|
|
2108
|
-
|
|
2109
|
-
export const { can, hasRole } = policy
|
|
2110
|
-
export type Role = RoleOf<typeof policy>
|
|
2111
|
-
export type Permission = PermissionOf<typeof policy>
|
|
2112
|
-
`;
|
|
2113
|
-
}
|
|
2114
|
-
async function pullPolicy(o = {}) {
|
|
2115
|
-
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
2116
|
-
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
2117
|
-
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
2118
|
-
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
2119
|
-
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
2120
|
-
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
2121
|
-
if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
|
|
2122
|
-
}
|
|
2123
|
-
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
2124
|
-
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
|
|
2125
|
-
});
|
|
2126
|
-
const body = await res.json().catch(() => null);
|
|
2127
|
-
if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
2128
|
-
return body?.data?.roles ?? [];
|
|
2129
|
-
}
|
|
2130
|
-
async function pushPolicy(o) {
|
|
2131
|
-
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
2132
|
-
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
2133
|
-
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
2134
|
-
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
2135
|
-
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
2136
|
-
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
2137
|
-
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
|
|
2138
|
-
}
|
|
2139
|
-
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
2140
|
-
method: "PUT",
|
|
2141
|
-
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
|
|
2142
|
-
body: JSON.stringify({ roles: o.roles })
|
|
2143
|
-
});
|
|
2144
|
-
if (!res.ok) {
|
|
2145
|
-
const body = await res.json().catch(() => null);
|
|
2146
|
-
throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
2367
|
// src/connect.ts
|
|
2151
2368
|
function sessionSecret() {
|
|
2152
2369
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -2271,7 +2488,9 @@ export {
|
|
|
2271
2488
|
passwordPolicyOf,
|
|
2272
2489
|
passwordProblems,
|
|
2273
2490
|
policySource,
|
|
2491
|
+
pullCatalog,
|
|
2274
2492
|
pullPolicy,
|
|
2493
|
+
pushCatalog,
|
|
2275
2494
|
pushPolicy,
|
|
2276
2495
|
pushTheme,
|
|
2277
2496
|
themeFromStackmyth,
|