cauthy-auth 0.28.1 → 0.31.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 +61 -3
- package/dist/{cauthy-CTVg4kvo.d.mts → cauthy-2jtP2LVQ.d.mts} +19 -1
- package/dist/{cauthy-BAwAJChs.d.ts → cauthy-CKfvvv_i.d.ts} +19 -1
- package/dist/cli.js +163 -59
- package/dist/client.d.mts +91 -10
- package/dist/client.d.ts +91 -10
- package/dist/client.js +39 -3
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +39 -3
- 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 +387 -148
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +385 -148
- 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 +275 -23
- package/dist/react.d.ts +275 -23
- package/dist/react.js +312 -78
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +311 -78
- package/dist/react.mjs.map +1 -1
- package/dist/styles.css +8 -0
- package/dist/svelte.d.mts +2 -2
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +53 -4
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +53 -4
- package/dist/svelte.mjs.map +1 -1
- package/dist/{system-CVBW_AV8.d.mts → system-BrrJsrFW.d.mts} +321 -109
- package/dist/{system-Bn8uckvQ.d.ts → system-BvvoNs2y.d.ts} +321 -109
- package/dist/vue.d.mts +2 -2
- package/dist/vue.d.ts +2 -2
- package/dist/vue.js +53 -4
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +53 -4
- 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,9 @@ 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") ?? "";
|
|
749
|
+
const acceptInviteUrl = given.acceptInviteUrl ?? env("CAUTHY_ACCEPT_INVITE_URL") ?? "";
|
|
750
|
+
const inviteHeaders = /^https?:\/\//.test(acceptInviteUrl) ? { "x-cauthy-client-id": clientId, "x-cauthy-link-to": acceptInviteUrl } : {};
|
|
537
751
|
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
538
752
|
const tokenEndpoint = `${issuer}/v1/oauth/token`;
|
|
539
753
|
function configured() {
|
|
@@ -576,10 +790,10 @@ function createSystem(given = {}) {
|
|
|
576
790
|
asking = void 0;
|
|
577
791
|
});
|
|
578
792
|
}
|
|
579
|
-
async function call(method, path, body) {
|
|
793
|
+
async function call(method, path, body, extra = {}) {
|
|
580
794
|
const send = async () => doFetch(`${issuer}${path}`, {
|
|
581
795
|
method,
|
|
582
|
-
headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
796
|
+
headers: { authorization: `Bearer ${await token()}`, ...extra, ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
583
797
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
584
798
|
});
|
|
585
799
|
let res = await send();
|
|
@@ -590,7 +804,7 @@ function createSystem(given = {}) {
|
|
|
590
804
|
if (!res.ok) await fail(res, `${method} ${path}`);
|
|
591
805
|
return res.status === 204 ? void 0 : res.json();
|
|
592
806
|
}
|
|
593
|
-
const data = async (method, path, body) => (await call(method, path, body))?.data;
|
|
807
|
+
const data = async (method, path, body, extra) => (await call(method, path, body, extra))?.data;
|
|
594
808
|
const id = encodeURIComponent;
|
|
595
809
|
return {
|
|
596
810
|
token,
|
|
@@ -627,7 +841,8 @@ function createSystem(given = {}) {
|
|
|
627
841
|
return toMember(await data(
|
|
628
842
|
"POST",
|
|
629
843
|
`/v1/organizations/${id(orgId)}/members`,
|
|
630
|
-
{ role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
|
|
844
|
+
{ role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId },
|
|
845
|
+
inviteHeaders
|
|
631
846
|
));
|
|
632
847
|
},
|
|
633
848
|
async setRole(orgId, userId, role) {
|
|
@@ -638,7 +853,7 @@ function createSystem(given = {}) {
|
|
|
638
853
|
}
|
|
639
854
|
},
|
|
640
855
|
async invite(person) {
|
|
641
|
-
const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
|
|
856
|
+
const u = await data("POST", "/v1/users", { ...person, client_id: clientId }, inviteHeaders);
|
|
642
857
|
return { id: u.id, email: u.email };
|
|
643
858
|
},
|
|
644
859
|
theme: {
|
|
@@ -651,6 +866,19 @@ function createSystem(given = {}) {
|
|
|
651
866
|
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
652
867
|
}
|
|
653
868
|
},
|
|
869
|
+
catalog: {
|
|
870
|
+
async get(system) {
|
|
871
|
+
return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
|
|
872
|
+
},
|
|
873
|
+
async set(system, resources) {
|
|
874
|
+
return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
async check(q) {
|
|
878
|
+
const system = q.system ?? ownSystem;
|
|
879
|
+
if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
|
|
880
|
+
return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
|
|
881
|
+
},
|
|
654
882
|
async publicJwks() {
|
|
655
883
|
if (!key) return { keys: [] };
|
|
656
884
|
const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
|
|
@@ -798,6 +1026,12 @@ function checkOrganization(value, required = false) {
|
|
|
798
1026
|
if (v.length > MAX_ORGANIZATION) return { field: "organization", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
799
1027
|
return null;
|
|
800
1028
|
}
|
|
1029
|
+
function checkOrganizationName(value) {
|
|
1030
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
1031
|
+
if (v === "") return { field: "name", code: "required", message: "Enter the organization's name." };
|
|
1032
|
+
if (v.length > MAX_ORGANIZATION) return { field: "name", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
801
1035
|
function checkCode(value) {
|
|
802
1036
|
const v = typeof value === "string" ? value.trim() : "";
|
|
803
1037
|
if (v === "") return { field: "code", code: "required", message: "Enter the code." };
|
|
@@ -929,6 +1163,9 @@ function redirect(location, cookies = []) {
|
|
|
929
1163
|
for (const c of cookies) h.append("set-cookie", c);
|
|
930
1164
|
return new Response(null, { status: 302, headers: h });
|
|
931
1165
|
}
|
|
1166
|
+
function conditionalOf(c) {
|
|
1167
|
+
return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
|
|
1168
|
+
}
|
|
932
1169
|
function createCauthy(given = {}) {
|
|
933
1170
|
const issuer = withoutTrailingSlashes(given.issuer ?? env2("CAUTHY_ISSUER") ?? "");
|
|
934
1171
|
const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
|
|
@@ -951,13 +1188,16 @@ function createCauthy(given = {}) {
|
|
|
951
1188
|
const againCookie = `${cookieName}_again`;
|
|
952
1189
|
const stepCookie = `${cookieName}_step`;
|
|
953
1190
|
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
1191
|
+
const iconOf = iconFor(issuer);
|
|
954
1192
|
const key = keyOf(secret);
|
|
955
1193
|
const pages = {
|
|
956
1194
|
signIn: given.pages?.signIn ? localPath(given.pages.signIn, "/") : void 0,
|
|
957
1195
|
resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
|
|
958
|
-
verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
|
|
1196
|
+
verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0,
|
|
1197
|
+
acceptInvite: given.pages?.acceptInvite ? localPath(given.pages.acceptInvite, "/") : void 0
|
|
959
1198
|
};
|
|
960
|
-
const
|
|
1199
|
+
const acceptInviteUrl = baseURL && pages.acceptInvite ? `${withoutTrailingSlashes(baseURL)}${pages.acceptInvite}` : void 0;
|
|
1200
|
+
const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, system: system || void 0, acceptInviteUrl, fetch: given.fetch });
|
|
961
1201
|
const originOf = (request) => {
|
|
962
1202
|
if (baseURL) return withoutTrailingSlashes(baseURL);
|
|
963
1203
|
const url = new URL(request.url);
|
|
@@ -1092,13 +1332,35 @@ function createCauthy(given = {}) {
|
|
|
1092
1332
|
role: String(o.role ?? ""),
|
|
1093
1333
|
metadata: o.metadata ?? {},
|
|
1094
1334
|
...o.external_id ? { externalId: String(o.external_id) } : {},
|
|
1095
|
-
...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [] } : {}
|
|
1335
|
+
...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) : [] } : {}
|
|
1096
1336
|
})
|
|
1097
1337
|
);
|
|
1098
1338
|
if (orgCache.size > 5e3) orgCache.clear();
|
|
1099
1339
|
orgCache.set(session.accessToken, { at: Date.now(), value });
|
|
1100
1340
|
return value;
|
|
1101
1341
|
}
|
|
1342
|
+
async function decide(token2, question) {
|
|
1343
|
+
const org = organizationIdOf(question.organization);
|
|
1344
|
+
if (org === "") return { allowed: false, reason: "no organization named" };
|
|
1345
|
+
const res = await doFetch(`${issuer}/v1/oauth/organizations/${encodeURIComponent(org)}/check`, {
|
|
1346
|
+
method: "POST",
|
|
1347
|
+
headers: { authorization: `Bearer ${token2}`, "content-type": "application/json", accept: "application/json" },
|
|
1348
|
+
body: JSON.stringify(checkWire(question))
|
|
1349
|
+
}).catch(() => null);
|
|
1350
|
+
if (res === null) throw new CauthyApiError(503, "unavailable", "Cauthy could not be reached to check a permission");
|
|
1351
|
+
const body2 = await res.json().catch(() => null);
|
|
1352
|
+
if (res.ok) return checkResultOf(body2?.data);
|
|
1353
|
+
const first = body2?.errors?.[0];
|
|
1354
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
1355
|
+
return { allowed: false, reason: first?.message ?? (res.status === 401 ? "not signed in" : res.status === 404 ? "not a member of the organization" : "refused") };
|
|
1356
|
+
}
|
|
1357
|
+
throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused to check a permission (${res.status}): ${first?.message ?? res.statusText}`);
|
|
1358
|
+
}
|
|
1359
|
+
async function check(who, question) {
|
|
1360
|
+
const session = "accessToken" in who ? who : await getSession(who);
|
|
1361
|
+
if (!session) return { allowed: false, reason: "not signed in" };
|
|
1362
|
+
return decide(session.accessToken, question);
|
|
1363
|
+
}
|
|
1102
1364
|
const forgetting = (fn) => async (...args) => {
|
|
1103
1365
|
const out2 = await fn(...args);
|
|
1104
1366
|
orgCache.clear();
|
|
@@ -1231,7 +1493,15 @@ function createCauthy(given = {}) {
|
|
|
1231
1493
|
name: String(d?.name ?? ""),
|
|
1232
1494
|
kind: String(d?.kind ?? ""),
|
|
1233
1495
|
role: String(d?.role ?? ""),
|
|
1234
|
-
|
|
1496
|
+
// A Cauthy from before a power says nothing of it: then it is not given.
|
|
1497
|
+
can: {
|
|
1498
|
+
readMembers: d?.can?.read_members === true,
|
|
1499
|
+
manageMembers: d?.can?.manage_members === true,
|
|
1500
|
+
removeMembers: d?.can?.remove_members === true,
|
|
1501
|
+
update: d?.can?.update === true,
|
|
1502
|
+
archive: d?.can?.archive === true,
|
|
1503
|
+
delete: d?.can?.delete === true
|
|
1504
|
+
}
|
|
1235
1505
|
});
|
|
1236
1506
|
const orgMemberOf = (d) => ({
|
|
1237
1507
|
userId: String(d?.user_id ?? ""),
|
|
@@ -1354,6 +1624,14 @@ function createCauthy(given = {}) {
|
|
|
1354
1624
|
for (const c of [...await write(request, s, readCookies(request.headers.get("cookie"))), ...extra]) h.append("set-cookie", c);
|
|
1355
1625
|
return json({ status: "signed_in", session: await toPublic(toSession(s)), ...more }, 200, h);
|
|
1356
1626
|
}
|
|
1627
|
+
async function loggedIn(request, data) {
|
|
1628
|
+
if (data?.mfa_required === true) {
|
|
1629
|
+
const step = { kind: "mfa", token: String(data.mfa_token ?? ""), returnTo: afterSignIn };
|
|
1630
|
+
const how = data.mfa_method === "email" ? { method: "email" } : {};
|
|
1631
|
+
return json({ status: "mfa_required", ...how }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
|
|
1632
|
+
}
|
|
1633
|
+
return signedIn(request, data);
|
|
1634
|
+
}
|
|
1357
1635
|
let lookCache = null;
|
|
1358
1636
|
let policyCache = null;
|
|
1359
1637
|
const keepPolicy = (settings) => {
|
|
@@ -1424,7 +1702,12 @@ function createCauthy(given = {}) {
|
|
|
1424
1702
|
"/me/organizations/members/add",
|
|
1425
1703
|
"/me/organizations/members/role",
|
|
1426
1704
|
"/me/organizations/members/remove",
|
|
1427
|
-
"/me/organizations/members/invite"
|
|
1705
|
+
"/me/organizations/members/invite",
|
|
1706
|
+
"/me/organizations/rename",
|
|
1707
|
+
"/me/organizations/archive",
|
|
1708
|
+
"/me/organizations/delete",
|
|
1709
|
+
"/invitation",
|
|
1710
|
+
"/invitation/accept"
|
|
1428
1711
|
].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";
|
|
1429
1712
|
if (!own) return null;
|
|
1430
1713
|
if (tenant === "") {
|
|
@@ -1446,17 +1729,21 @@ function createCauthy(given = {}) {
|
|
|
1446
1729
|
]);
|
|
1447
1730
|
const list = Array.isArray(providers.data) ? providers.data : [];
|
|
1448
1731
|
const password = keepPolicy(settings);
|
|
1449
|
-
const
|
|
1732
|
+
const passkeys = settings.data?.allow_passkeys !== false;
|
|
1733
|
+
const here = passkeys && place.data?.here === true;
|
|
1450
1734
|
return json({
|
|
1451
|
-
providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
|
|
1735
|
+
providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? ""), ...iconOf(p) })).filter((p) => p.id !== ""),
|
|
1736
|
+
// Whether the workspace signs people in with a password at all. A
|
|
1737
|
+
// Cauthy from before the setting says nothing of it: then it does.
|
|
1738
|
+
passwordSignIn: settings.data?.allow_password !== false,
|
|
1452
1739
|
signUp: settings.data?.allow_signup !== false,
|
|
1453
1740
|
// The application's system makes an organization per sign-up: the form asks the company's name.
|
|
1454
1741
|
...settings.data?.signup?.organization?.ask === true ? { organization: { ask: true, required: settings.data.signup.organization.required === true } } : {},
|
|
1455
1742
|
// A sign-in link needs a page of the application to open.
|
|
1456
1743
|
magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
|
|
1457
|
-
passkey: place.error ? true : here || place.data?.on_cauthy === true,
|
|
1744
|
+
passkey: passkeys && (place.error ? true : here || place.data?.on_cauthy === true),
|
|
1458
1745
|
passkeyHere: here,
|
|
1459
|
-
passkeyOnCauthy: place.error ? true : place.data?.on_cauthy === true,
|
|
1746
|
+
passkeyOnCauthy: passkeys && (place.error ? true : place.data?.on_cauthy === true),
|
|
1460
1747
|
// A company's sign-in, found by the person's work address.
|
|
1461
1748
|
sso: Array.isArray(sso.data) && sso.data.some((c) => c?.discoverable === true),
|
|
1462
1749
|
// The terms a sign-up accepts and the consents it answers, when the workspace asks any.
|
|
@@ -1466,6 +1753,14 @@ function createCauthy(given = {}) {
|
|
|
1466
1753
|
...password ? { password } : {}
|
|
1467
1754
|
});
|
|
1468
1755
|
}
|
|
1756
|
+
if (route === "/invitation" && !post) {
|
|
1757
|
+
const token2 = (url.searchParams.get("token") ?? "").trim();
|
|
1758
|
+
if (token2 === "" || token2.length > 512) return problem(422, "invalid_token", "This invitation is not valid, or it has expired.");
|
|
1759
|
+
const res = await ask(request, "GET", `/v1/auth/invitation?token=${encodeURIComponent(token2)}`);
|
|
1760
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1761
|
+
const organization = typeof res.data?.organization === "string" && res.data.organization !== "" ? { organization: res.data.organization } : {};
|
|
1762
|
+
return json({ status: "ok", invitation: { email: String(res.data?.email ?? ""), workspace: String(res.data?.workspace ?? ""), ...organization } });
|
|
1763
|
+
}
|
|
1469
1764
|
if (route === "/sign-in/sso" && !post) {
|
|
1470
1765
|
const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
|
|
1471
1766
|
const failed2 = (code) => redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code)}`);
|
|
@@ -1695,12 +1990,7 @@ function createCauthy(given = {}) {
|
|
|
1695
1990
|
if (bad) return invalid(bad);
|
|
1696
1991
|
const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password });
|
|
1697
1992
|
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1698
|
-
|
|
1699
|
-
const step = { kind: "mfa", token: String(res.data.mfa_token ?? ""), returnTo: afterSignIn };
|
|
1700
|
-
const how = res.data.mfa_method === "email" ? { method: "email" } : {};
|
|
1701
|
-
return json({ status: "mfa_required", ...how }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
|
|
1702
|
-
}
|
|
1703
|
-
return signedIn(request, res.data);
|
|
1993
|
+
return loggedIn(request, res.data);
|
|
1704
1994
|
}
|
|
1705
1995
|
if (route === "/sign-in/mfa") {
|
|
1706
1996
|
const bad = checkCode(b.code);
|
|
@@ -1895,6 +2185,23 @@ function createCauthy(given = {}) {
|
|
|
1895
2185
|
if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
|
|
1896
2186
|
return signedIn(request, res.data);
|
|
1897
2187
|
}
|
|
2188
|
+
if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
|
|
2189
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
2190
|
+
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
2191
|
+
const path = `/v1/auth/me/organizations/${org}`;
|
|
2192
|
+
if (route === "/me/organizations/rename") {
|
|
2193
|
+
const bad = checkOrganizationName(b.name);
|
|
2194
|
+
if (bad) return invalid(bad);
|
|
2195
|
+
const res2 = await as(path, { name: String(b.name).trim() }, "PATCH");
|
|
2196
|
+
if (res2.error) return refused(res2, res2.error.code === "validation_error" ? "name" : void 0);
|
|
2197
|
+
orgCache.delete(mine.token);
|
|
2198
|
+
return json({ status: "renamed", organization: seatOf(res2.data) }, 200, mine.headers);
|
|
2199
|
+
}
|
|
2200
|
+
const res = route === "/me/organizations/archive" ? await as(`${path}/archive`) : await as(path, void 0, "DELETE");
|
|
2201
|
+
if (res.error) return refused(res);
|
|
2202
|
+
orgCache.delete(mine.token);
|
|
2203
|
+
return json({ status: route === "/me/organizations/archive" ? "archived" : "deleted", organizationId: org }, 200, mine.headers);
|
|
2204
|
+
}
|
|
1898
2205
|
if (route.startsWith("/me/organizations/members/")) {
|
|
1899
2206
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
1900
2207
|
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
@@ -1904,13 +2211,13 @@ function createCauthy(given = {}) {
|
|
|
1904
2211
|
if (bad) return invalid(bad);
|
|
1905
2212
|
const role = typeof b.role === "string" ? b.role : "";
|
|
1906
2213
|
if (role === "") return problem(400, "invalid_role", "Choose a role.", "role", mine.headers);
|
|
1907
|
-
const res = await
|
|
2214
|
+
const res = await ask(request, "POST", base, {
|
|
1908
2215
|
email: String(b.email).trim(),
|
|
1909
2216
|
role,
|
|
1910
2217
|
name: typeof b.name === "string" ? b.name.trim() : "",
|
|
1911
2218
|
client_id: clientId,
|
|
1912
2219
|
...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {}
|
|
1913
|
-
});
|
|
2220
|
+
}, pages.acceptInvite, mine.token);
|
|
1914
2221
|
if (res.error) return refused(res, res.error.code === "unknown_role" ? "role" : "email");
|
|
1915
2222
|
return json({ status: "added", member: orgMemberOf(res.data) }, 200, mine.headers);
|
|
1916
2223
|
}
|
|
@@ -1930,7 +2237,7 @@ function createCauthy(given = {}) {
|
|
|
1930
2237
|
return json({ status: "removed", userId: user }, 200, mine.headers);
|
|
1931
2238
|
}
|
|
1932
2239
|
if (route === "/me/organizations/members/invite") {
|
|
1933
|
-
const res = await
|
|
2240
|
+
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);
|
|
1934
2241
|
if (res.error) return refused(res);
|
|
1935
2242
|
return json({ status: "sent", userId: user }, 200, mine.headers);
|
|
1936
2243
|
}
|
|
@@ -1969,6 +2276,14 @@ function createCauthy(given = {}) {
|
|
|
1969
2276
|
if (res.data?.access_token) return signedIn(request, res.data);
|
|
1970
2277
|
return json({ status: "verified" });
|
|
1971
2278
|
}
|
|
2279
|
+
if (route === "/invitation/accept") {
|
|
2280
|
+
if (!isToken(b.token)) return problem(422, "invalid_token", "This invitation is not valid, or it has expired.");
|
|
2281
|
+
const bad = await newPasswordProblem(request, b.password);
|
|
2282
|
+
if (bad) return invalid(bad);
|
|
2283
|
+
const res = await ask(request, "POST", "/v1/auth/invitation/accept", { token: String(b.token), password: b.password });
|
|
2284
|
+
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);
|
|
2285
|
+
return loggedIn(request, res.data);
|
|
2286
|
+
}
|
|
1972
2287
|
if (route === "/sign-in/passkey/begin") {
|
|
1973
2288
|
const email = typeof b.email === "string" && checkEmail(b.email) === null ? b.email.trim() : "";
|
|
1974
2289
|
const res = await ask(request, "POST", "/v1/auth/webauthn/login/begin", email !== "" ? { email } : {});
|
|
@@ -2040,6 +2355,7 @@ function createCauthy(given = {}) {
|
|
|
2040
2355
|
const get = request.method === "GET" || request.method === "HEAD";
|
|
2041
2356
|
const mine = await ownRoute(request, url, route);
|
|
2042
2357
|
if (mine !== null) return mine;
|
|
2358
|
+
if (route === "/me/organizations/check") return checkRoute(request);
|
|
2043
2359
|
if (route === "/login" && get) return login(request, url);
|
|
2044
2360
|
if (route === "/callback" && get) return callback(request, url);
|
|
2045
2361
|
if (route === "/logout" && (get || request.method === "POST")) return logout(request);
|
|
@@ -2066,6 +2382,36 @@ function createCauthy(given = {}) {
|
|
|
2066
2382
|
}
|
|
2067
2383
|
return new Response("Not found", { status: 404 });
|
|
2068
2384
|
}
|
|
2385
|
+
async function checkRoute(request) {
|
|
2386
|
+
if (request.method !== "POST") return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
2387
|
+
if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
|
|
2388
|
+
const b = await body(request);
|
|
2389
|
+
if (b === null) return problem(400, "bad_request", "Send the question as JSON.");
|
|
2390
|
+
const asker = await signedInAs(request);
|
|
2391
|
+
if ("response" in asker) return asker.response;
|
|
2392
|
+
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
2393
|
+
if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
|
|
2394
|
+
const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
|
|
2395
|
+
if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
|
|
2396
|
+
const attributes2 = b.attributes;
|
|
2397
|
+
if (attributes2 !== void 0 && (typeof attributes2 !== "object" || attributes2 === null || Array.isArray(attributes2))) {
|
|
2398
|
+
return problem(400, "bad_request", "The attributes are an object.", void 0, asker.headers);
|
|
2399
|
+
}
|
|
2400
|
+
try {
|
|
2401
|
+
const r = await decide(asker.token, {
|
|
2402
|
+
organization: org,
|
|
2403
|
+
resource: b.resource,
|
|
2404
|
+
action: b.action,
|
|
2405
|
+
...attributes2 !== void 0 ? { attributes: attributes2 } : {},
|
|
2406
|
+
...typeof b.resourceId === "string" ? { resourceId: b.resourceId } : {},
|
|
2407
|
+
...typeof b.ownerId === "string" ? { ownerId: b.ownerId } : {}
|
|
2408
|
+
});
|
|
2409
|
+
return json({ status: "decided", ...r }, 200, asker.headers);
|
|
2410
|
+
} catch (e) {
|
|
2411
|
+
const err = e instanceof CauthyApiError ? e : new CauthyApiError(503, "unavailable", "Cauthy could not be reached.");
|
|
2412
|
+
return problem(err.status, err.code, err.message.replace(/^cauthy-auth: /, ""), void 0, asker.headers);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2069
2415
|
async function protect(request) {
|
|
2070
2416
|
const { value } = await read(request);
|
|
2071
2417
|
if (value !== null && fresh(value)) return { session: toSession(value) };
|
|
@@ -2089,11 +2435,24 @@ function createCauthy(given = {}) {
|
|
|
2089
2435
|
resolve: resolve2,
|
|
2090
2436
|
protect,
|
|
2091
2437
|
organizations,
|
|
2438
|
+
check,
|
|
2092
2439
|
toPublic,
|
|
2093
2440
|
urls,
|
|
2094
2441
|
options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
|
|
2095
2442
|
};
|
|
2096
2443
|
}
|
|
2444
|
+
function iconFor(issuer) {
|
|
2445
|
+
return (p) => {
|
|
2446
|
+
const raw = typeof p?.icon_url === "string" ? p.icon_url.trim() : "";
|
|
2447
|
+
if (raw === "") return {};
|
|
2448
|
+
try {
|
|
2449
|
+
const url = new URL(raw.startsWith("/") && !raw.startsWith("//") ? `${issuer}${raw}` : raw);
|
|
2450
|
+
return url.protocol === "https:" || url.protocol === "http:" ? { iconUrl: url.href } : {};
|
|
2451
|
+
} catch {
|
|
2452
|
+
return {};
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2097
2456
|
function legalOf(raw) {
|
|
2098
2457
|
const documents = Array.isArray(raw?.documents) ? raw.documents : [];
|
|
2099
2458
|
const purposes = Array.isArray(raw?.consent_purposes) ? raw.consent_purposes : [];
|
|
@@ -2103,128 +2462,6 @@ function legalOf(raw) {
|
|
|
2103
2462
|
};
|
|
2104
2463
|
}
|
|
2105
2464
|
|
|
2106
|
-
// src/permissions.ts
|
|
2107
|
-
function matches(entry, resource, action) {
|
|
2108
|
-
const i = entry.lastIndexOf(":");
|
|
2109
|
-
if (i < 0) return false;
|
|
2110
|
-
const r = entry.slice(0, i);
|
|
2111
|
-
const a = entry.slice(i + 1);
|
|
2112
|
-
return (r === "*" || r === resource) && (a === "*" || a === action);
|
|
2113
|
-
}
|
|
2114
|
-
function can(organization, resource, action) {
|
|
2115
|
-
if (!organization?.permissions) return false;
|
|
2116
|
-
if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
|
|
2117
|
-
return organization.permissions.some((p) => matches(p, resource, action));
|
|
2118
|
-
}
|
|
2119
|
-
function definePolicy(roles) {
|
|
2120
|
-
const entry = (p) => `${p.resource}:${p.action}`;
|
|
2121
|
-
const byName = new Map(roles.map((r) => [r.role, r]));
|
|
2122
|
-
const split = (permission) => {
|
|
2123
|
-
const i = permission.lastIndexOf(":");
|
|
2124
|
-
return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
|
|
2125
|
-
};
|
|
2126
|
-
return {
|
|
2127
|
-
roles: roles.map((r) => ({
|
|
2128
|
-
role: r.role,
|
|
2129
|
-
...r.name !== void 0 ? { name: r.name } : {},
|
|
2130
|
-
...r.description !== void 0 ? { description: r.description } : {},
|
|
2131
|
-
...r.inherits ? { inherits: [...r.inherits] } : {},
|
|
2132
|
-
...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
|
|
2133
|
-
})),
|
|
2134
|
-
roleNames: roles.map((r) => r.role),
|
|
2135
|
-
permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
|
|
2136
|
-
can(organization, permission) {
|
|
2137
|
-
const [resource, action] = split(permission);
|
|
2138
|
-
return can(organization, resource, action);
|
|
2139
|
-
},
|
|
2140
|
-
hasRole(organization, ...wanted) {
|
|
2141
|
-
return organization != null && wanted.includes(organization.role);
|
|
2142
|
-
},
|
|
2143
|
-
permissionsOf(role) {
|
|
2144
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2145
|
-
const out2 = /* @__PURE__ */ new Set();
|
|
2146
|
-
const walk = (name) => {
|
|
2147
|
-
if (seen.has(name)) return;
|
|
2148
|
-
seen.add(name);
|
|
2149
|
-
const r = byName.get(name);
|
|
2150
|
-
for (const p of r?.permissions ?? []) out2.add(entry(p));
|
|
2151
|
-
for (const parent of r?.inherits ?? []) walk(parent);
|
|
2152
|
-
};
|
|
2153
|
-
walk(role);
|
|
2154
|
-
return [...out2];
|
|
2155
|
-
}
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
|
-
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2159
|
-
function policySource(system, roles) {
|
|
2160
|
-
const text = (v) => JSON.stringify(v);
|
|
2161
|
-
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
2162
|
-
const one = (r) => {
|
|
2163
|
-
const parts = [`role: ${text(r.role)}`];
|
|
2164
|
-
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
2165
|
-
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
2166
|
-
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
2167
|
-
if (r.permissions?.length) {
|
|
2168
|
-
const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
|
|
2169
|
-
parts.push(`permissions: [
|
|
2170
|
-
${list.join("\n")}
|
|
2171
|
-
]`);
|
|
2172
|
-
}
|
|
2173
|
-
return ` {
|
|
2174
|
-
${parts.join(",\n ")},
|
|
2175
|
-
},`;
|
|
2176
|
-
};
|
|
2177
|
-
return `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
2178
|
-
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
2179
|
-
// changes, and do not edit this file by hand.
|
|
2180
|
-
|
|
2181
|
-
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
2182
|
-
|
|
2183
|
-
export const policy = definePolicy([
|
|
2184
|
-
${roles.map(one).join("\n")}
|
|
2185
|
-
])
|
|
2186
|
-
|
|
2187
|
-
export const { can, hasRole } = policy
|
|
2188
|
-
export type Role = RoleOf<typeof policy>
|
|
2189
|
-
export type Permission = PermissionOf<typeof policy>
|
|
2190
|
-
`;
|
|
2191
|
-
}
|
|
2192
|
-
async function pullPolicy(o = {}) {
|
|
2193
|
-
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
2194
|
-
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
2195
|
-
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
2196
|
-
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
2197
|
-
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
2198
|
-
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
2199
|
-
if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
|
|
2200
|
-
}
|
|
2201
|
-
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
2202
|
-
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
|
|
2203
|
-
});
|
|
2204
|
-
const body = await res.json().catch(() => null);
|
|
2205
|
-
if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
2206
|
-
return body?.data?.roles ?? [];
|
|
2207
|
-
}
|
|
2208
|
-
async function pushPolicy(o) {
|
|
2209
|
-
const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
2210
|
-
const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
|
|
2211
|
-
const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
|
|
2212
|
-
const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
|
|
2213
|
-
const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
|
|
2214
|
-
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
2215
|
-
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
|
|
2216
|
-
}
|
|
2217
|
-
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
2218
|
-
method: "PUT",
|
|
2219
|
-
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
|
|
2220
|
-
body: JSON.stringify({ roles: o.roles })
|
|
2221
|
-
});
|
|
2222
|
-
if (!res.ok) {
|
|
2223
|
-
const body = await res.json().catch(() => null);
|
|
2224
|
-
throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
2465
|
// src/connect.ts
|
|
2229
2466
|
function sessionSecret() {
|
|
2230
2467
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -2350,7 +2587,9 @@ async function verifyToken(from, options = {}) {
|
|
|
2350
2587
|
passwordPolicyOf,
|
|
2351
2588
|
passwordProblems,
|
|
2352
2589
|
policySource,
|
|
2590
|
+
pullCatalog,
|
|
2353
2591
|
pullPolicy,
|
|
2592
|
+
pushCatalog,
|
|
2354
2593
|
pushPolicy,
|
|
2355
2594
|
pushTheme,
|
|
2356
2595
|
themeFromStackmyth,
|