cauthy-auth 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -20,10 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ CauthyApiError: () => CauthyApiError,
23
24
  CauthyConfigError: () => CauthyConfigError,
24
25
  CauthyWebhookError: () => CauthyWebhookError,
25
26
  can: () => can,
27
+ connectSystem: () => connectSystem,
26
28
  createCauthy: () => createCauthy,
29
+ createSystem: () => createSystem,
27
30
  pushPolicy: () => pushPolicy,
28
31
  pushTheme: () => pushTheme,
29
32
  themeFromStackmyth: () => themeFromStackmyth,
@@ -87,8 +90,179 @@ async function unseal(sealed, key) {
87
90
  }
88
91
  }
89
92
 
90
- // src/webhook.ts
93
+ // src/system.ts
91
94
  var import_jose2 = require("jose");
95
+
96
+ // src/types.ts
97
+ var CauthyConfigError = class extends Error {
98
+ constructor(message) {
99
+ super(`cauthy-auth: ${message}`);
100
+ this.name = "CauthyConfigError";
101
+ }
102
+ };
103
+
104
+ // src/system.ts
105
+ var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
106
+ var SKEW_MS = 3e4;
107
+ var CauthyApiError = class extends Error {
108
+ constructor(status, code, message) {
109
+ super(`cauthy-auth: ${message}`);
110
+ this.status = status;
111
+ this.code = code;
112
+ this.name = "CauthyApiError";
113
+ }
114
+ status;
115
+ code;
116
+ };
117
+ function env(name) {
118
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
119
+ }
120
+ function toOrganization(o) {
121
+ return {
122
+ id: o.id,
123
+ system: o.system,
124
+ name: o.name,
125
+ kind: o.kind,
126
+ externalId: o.external_id,
127
+ status: o.status,
128
+ metadata: o.metadata ?? {},
129
+ members: o.members,
130
+ createdAt: o.created_at,
131
+ updatedAt: o.updated_at
132
+ };
133
+ }
134
+ function toMember(m) {
135
+ return { userId: m.user_id, email: m.email, name: m.name, status: m.status, role: m.role, ...m.invited === void 0 ? {} : { invited: m.invited } };
136
+ }
137
+ function asJwk(k) {
138
+ if (typeof k.privateKey !== "string") return k.privateKey;
139
+ return k.privateKey.includes("BEGIN") ? void 0 : JSON.parse(k.privateKey);
140
+ }
141
+ async function loadKey(k) {
142
+ const jwk = asJwk(k);
143
+ return jwk ? (0, import_jose2.importJWK)(jwk, "RS256") : (0, import_jose2.importPKCS8)(k.privateKey, "RS256", { extractable: true });
144
+ }
145
+ function createSystem(given = {}) {
146
+ const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
147
+ const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
148
+ const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
149
+ const envKey = env("CAUTHY_SYSTEM_KEY");
150
+ const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
151
+ const doFetch = (...args) => (given.fetch ?? fetch)(...args);
152
+ const tokenEndpoint = `${issuer}/v1/oauth/token`;
153
+ function configured() {
154
+ if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
155
+ if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
156
+ if (key ? key.keyId === "" : clientSecret === "") {
157
+ throw new CauthyConfigError(key ? "the key needs its id (CAUTHY_SYSTEM_KEY_ID)" : "to act on its own the application needs its secret (CAUTHY_CLIENT_SECRET) or a key (CAUTHY_SYSTEM_KEY)");
158
+ }
159
+ }
160
+ let imported;
161
+ const signingKey = () => imported ??= loadKey(key);
162
+ async function fail(res, what) {
163
+ const body = await res.json().catch(() => null);
164
+ const first = body?.errors?.[0];
165
+ throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused ${what} (${res.status}): ${first?.message ?? res.statusText}`);
166
+ }
167
+ let held;
168
+ let asking;
169
+ async function ask() {
170
+ configured();
171
+ const form = new URLSearchParams({ grant_type: "client_credentials", client_id: clientId });
172
+ if (key) {
173
+ const assertion = await new import_jose2.SignJWT({}).setProtectedHeader({ alg: "RS256", kid: key.keyId }).setIssuer(clientId).setSubject(clientId).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(await signingKey());
174
+ form.set("client_assertion_type", ASSERTION_TYPE);
175
+ form.set("client_assertion", assertion);
176
+ } else {
177
+ form.set("client_secret", clientSecret);
178
+ }
179
+ const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
180
+ if (!res.ok) await fail(res, "the application's own token");
181
+ const t = await res.json();
182
+ held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
183
+ return t.access_token;
184
+ }
185
+ function token() {
186
+ if (held && Date.now() < held.until) return Promise.resolve(held.value);
187
+ return asking ??= ask().finally(() => {
188
+ asking = void 0;
189
+ });
190
+ }
191
+ async function call(method, path, body) {
192
+ const send = async () => doFetch(`${issuer}${path}`, {
193
+ method,
194
+ headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
195
+ body: body === void 0 ? void 0 : JSON.stringify(body)
196
+ });
197
+ let res = await send();
198
+ if (res.status === 401 && held) {
199
+ held = void 0;
200
+ res = await send();
201
+ }
202
+ if (!res.ok) await fail(res, `${method} ${path}`);
203
+ return res.status === 204 ? void 0 : res.json();
204
+ }
205
+ const data = async (method, path, body) => (await call(method, path, body))?.data;
206
+ const id = encodeURIComponent;
207
+ return {
208
+ token,
209
+ request: data,
210
+ organizations: {
211
+ async list(query = {}) {
212
+ const q = new URLSearchParams();
213
+ for (const [k, v] of Object.entries(query)) if (v !== void 0) q.set(k, String(v));
214
+ const out2 = await call("GET", `/v1/organizations${q.size ? `?${q}` : ""}`);
215
+ return { items: out2.data.map(toOrganization), total: out2.total_count };
216
+ },
217
+ async create(input) {
218
+ return toOrganization(await data(
219
+ "POST",
220
+ "/v1/organizations",
221
+ { name: input.name, kind: input.kind, external_id: input.externalId, metadata: input.metadata }
222
+ ));
223
+ },
224
+ async get(orgId) {
225
+ return toOrganization(await data("GET", `/v1/organizations/${id(orgId)}`));
226
+ },
227
+ async update(orgId, patch) {
228
+ return toOrganization(await data("PATCH", `/v1/organizations/${id(orgId)}`, patch));
229
+ },
230
+ async remove(orgId) {
231
+ await call("DELETE", `/v1/organizations/${id(orgId)}`);
232
+ }
233
+ },
234
+ members: {
235
+ async list(orgId) {
236
+ return (await data("GET", `/v1/organizations/${id(orgId)}/members`)).map(toMember);
237
+ },
238
+ async add(orgId, m) {
239
+ return toMember(await data(
240
+ "POST",
241
+ `/v1/organizations/${id(orgId)}/members`,
242
+ { role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
243
+ ));
244
+ },
245
+ async setRole(orgId, userId, role) {
246
+ return toMember(await data("PUT", `/v1/organizations/${id(orgId)}/members/${id(userId)}`, { role }));
247
+ },
248
+ async remove(orgId, userId) {
249
+ await call("DELETE", `/v1/organizations/${id(orgId)}/members/${id(userId)}`);
250
+ }
251
+ },
252
+ async invite(person) {
253
+ const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
254
+ return { id: u.id, email: u.email };
255
+ },
256
+ async publicJwks() {
257
+ if (!key) return { keys: [] };
258
+ const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
259
+ return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
260
+ }
261
+ };
262
+ }
263
+
264
+ // src/webhook.ts
265
+ var import_jose3 = require("jose");
92
266
  var CauthyWebhookError = class extends Error {
93
267
  constructor(reason) {
94
268
  super(`cauthy-auth: webhook refused: ${reason}`);
@@ -101,15 +275,15 @@ var keys = /* @__PURE__ */ new Map();
101
275
  function keysOf(issuer) {
102
276
  let set = keys.get(issuer);
103
277
  if (set === void 0) {
104
- set = (0, import_jose2.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
278
+ set = (0, import_jose3.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
105
279
  keys.set(issuer, set);
106
280
  }
107
281
  return set;
108
282
  }
109
283
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
110
284
  async function verifyWebhook(request, options = {}) {
111
- const env2 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
112
- const issuer = (options.issuer ?? env2 ?? "").replace(/\/+$/, "");
285
+ const env3 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
286
+ const issuer = (options.issuer ?? env3 ?? "").replace(/\/+$/, "");
113
287
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
114
288
  const signature = request.headers.get("x-cauthy-signature-jwt");
115
289
  if (!signature) throw new CauthyWebhookError("no signature");
@@ -117,7 +291,7 @@ async function verifyWebhook(request, options = {}) {
117
291
  const audience = options.url ?? request.url.split("?")[0];
118
292
  let claims;
119
293
  try {
120
- const verified = await (0, import_jose2.jwtVerify)(signature, options.jwks ?? keysOf(issuer), {
294
+ const verified = await (0, import_jose3.jwtVerify)(signature, options.jwks ?? keysOf(issuer), {
121
295
  algorithms: ["RS256"],
122
296
  issuer,
123
297
  audience,
@@ -158,20 +332,12 @@ async function verifyWebhook(request, options = {}) {
158
332
  };
159
333
  }
160
334
 
161
- // src/types.ts
162
- var CauthyConfigError = class extends Error {
163
- constructor(message) {
164
- super(`cauthy-auth: ${message}`);
165
- this.name = "CauthyConfigError";
166
- }
167
- };
168
-
169
335
  // src/cauthy.ts
170
336
  var SESSION_DAYS = 30;
171
337
  var FLOW_SECONDS = 600;
172
338
  var SKEW_SECONDS = 30;
173
339
  var ORGANIZATIONS_TTL_MS = 6e4;
174
- function env(name) {
340
+ function env2(name) {
175
341
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
176
342
  }
177
343
  function b64url(bytes) {
@@ -198,11 +364,11 @@ function redirect(location, cookies = []) {
198
364
  return new Response(null, { status: 302, headers: h });
199
365
  }
200
366
  function createCauthy(given = {}) {
201
- const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
202
- const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
203
- const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET");
204
- const secret = given.secret ?? env("CAUTHY_SECRET") ?? "";
205
- const system = given.system ?? env("CAUTHY_SYSTEM") ?? "";
367
+ const issuer = (given.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
368
+ const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
369
+ const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
370
+ const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
371
+ const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
206
372
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
207
373
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
208
374
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
@@ -471,6 +637,7 @@ function createCauthy(given = {}) {
471
637
  return {
472
638
  handler,
473
639
  webhook,
640
+ system: createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch }),
474
641
  getSession,
475
642
  status,
476
643
  resolve: resolve2,
@@ -683,11 +850,11 @@ function toWire(theme) {
683
850
  return wire;
684
851
  }
685
852
  async function pushTheme(o) {
686
- const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
687
- const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
688
- const clientId = o.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
689
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
690
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
853
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
854
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
855
+ const clientId = o.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
856
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
857
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
691
858
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
692
859
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
693
860
  }
@@ -716,11 +883,11 @@ function can(organization, resource, action) {
716
883
  return organization.permissions.some((p) => matches(p, resource, action));
717
884
  }
718
885
  async function pushPolicy(o) {
719
- const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
720
- const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
721
- const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
722
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
723
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
886
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
887
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
888
+ const system = o.system ?? env3("CAUTHY_SYSTEM") ?? "";
889
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
890
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
724
891
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
725
892
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
726
893
  }
@@ -734,12 +901,59 @@ async function pushPolicy(o) {
734
901
  throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
735
902
  }
736
903
  }
904
+
905
+ // src/connect.ts
906
+ function sessionSecret() {
907
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
908
+ let s = "";
909
+ for (const b of bytes) s += String.fromCharCode(b);
910
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
911
+ }
912
+ async function connectSystem(o) {
913
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
914
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
915
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
916
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
917
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
918
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
919
+ }
920
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/connect`, {
921
+ method: "POST",
922
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
923
+ body: JSON.stringify({
924
+ system: o.system,
925
+ app_url: o.appUrl,
926
+ name: o.name,
927
+ callback_path: o.callbackPath,
928
+ events_url: o.eventsUrl,
929
+ events: o.events,
930
+ jwks_uri: o.jwksUri
931
+ })
932
+ });
933
+ const body = await res.json().catch(() => null);
934
+ if (!res.ok || !body?.data) {
935
+ const why = (body?.errors ?? []).map((e) => e.message).filter(Boolean).join("; ") || res.statusText;
936
+ throw new Error(`cauthy-auth: Cauthy did not connect the system (${res.status}): ${why}`);
937
+ }
938
+ const d = body.data;
939
+ return {
940
+ system: d.system,
941
+ clientId: d.client.client_id,
942
+ clientSecret: d.client_secret,
943
+ redirectUris: d.client.redirect_uris,
944
+ webhook: d.webhook,
945
+ env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
946
+ };
947
+ }
737
948
  // Annotate the CommonJS export names for ESM import in node:
738
949
  0 && (module.exports = {
950
+ CauthyApiError,
739
951
  CauthyConfigError,
740
952
  CauthyWebhookError,
741
953
  can,
954
+ connectSystem,
742
955
  createCauthy,
956
+ createSystem,
743
957
  pushPolicy,
744
958
  pushTheme,
745
959
  themeFromStackmyth,