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.mjs CHANGED
@@ -54,6 +54,177 @@ 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
+ // src/types.ts
61
+ var CauthyConfigError = class extends Error {
62
+ constructor(message) {
63
+ super(`cauthy-auth: ${message}`);
64
+ this.name = "CauthyConfigError";
65
+ }
66
+ };
67
+
68
+ // src/system.ts
69
+ var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
70
+ var SKEW_MS = 3e4;
71
+ var CauthyApiError = class extends Error {
72
+ constructor(status, code, message) {
73
+ super(`cauthy-auth: ${message}`);
74
+ this.status = status;
75
+ this.code = code;
76
+ this.name = "CauthyApiError";
77
+ }
78
+ status;
79
+ code;
80
+ };
81
+ function env(name) {
82
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
83
+ }
84
+ function toOrganization(o) {
85
+ return {
86
+ id: o.id,
87
+ system: o.system,
88
+ name: o.name,
89
+ kind: o.kind,
90
+ externalId: o.external_id,
91
+ status: o.status,
92
+ metadata: o.metadata ?? {},
93
+ members: o.members,
94
+ createdAt: o.created_at,
95
+ updatedAt: o.updated_at
96
+ };
97
+ }
98
+ function toMember(m) {
99
+ return { userId: m.user_id, email: m.email, name: m.name, status: m.status, role: m.role, ...m.invited === void 0 ? {} : { invited: m.invited } };
100
+ }
101
+ function asJwk(k) {
102
+ if (typeof k.privateKey !== "string") return k.privateKey;
103
+ return k.privateKey.includes("BEGIN") ? void 0 : JSON.parse(k.privateKey);
104
+ }
105
+ async function loadKey(k) {
106
+ const jwk = asJwk(k);
107
+ return jwk ? importJWK(jwk, "RS256") : importPKCS8(k.privateKey, "RS256", { extractable: true });
108
+ }
109
+ function createSystem(given = {}) {
110
+ const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
111
+ const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
112
+ const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
113
+ const envKey = env("CAUTHY_SYSTEM_KEY");
114
+ const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
115
+ const doFetch = (...args) => (given.fetch ?? fetch)(...args);
116
+ const tokenEndpoint = `${issuer}/v1/oauth/token`;
117
+ function configured() {
118
+ if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
119
+ if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
120
+ if (key ? key.keyId === "" : clientSecret === "") {
121
+ 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)");
122
+ }
123
+ }
124
+ let imported;
125
+ const signingKey = () => imported ??= loadKey(key);
126
+ async function fail(res, what) {
127
+ const body = await res.json().catch(() => null);
128
+ const first = body?.errors?.[0];
129
+ throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused ${what} (${res.status}): ${first?.message ?? res.statusText}`);
130
+ }
131
+ let held;
132
+ let asking;
133
+ async function ask() {
134
+ configured();
135
+ const form = new URLSearchParams({ grant_type: "client_credentials", client_id: clientId });
136
+ if (key) {
137
+ const assertion = await new SignJWT({}).setProtectedHeader({ alg: "RS256", kid: key.keyId }).setIssuer(clientId).setSubject(clientId).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(await signingKey());
138
+ form.set("client_assertion_type", ASSERTION_TYPE);
139
+ form.set("client_assertion", assertion);
140
+ } else {
141
+ form.set("client_secret", clientSecret);
142
+ }
143
+ const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
144
+ if (!res.ok) await fail(res, "the application's own token");
145
+ const t = await res.json();
146
+ held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
147
+ return t.access_token;
148
+ }
149
+ function token() {
150
+ if (held && Date.now() < held.until) return Promise.resolve(held.value);
151
+ return asking ??= ask().finally(() => {
152
+ asking = void 0;
153
+ });
154
+ }
155
+ async function call(method, path, body) {
156
+ const send = async () => doFetch(`${issuer}${path}`, {
157
+ method,
158
+ headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
159
+ body: body === void 0 ? void 0 : JSON.stringify(body)
160
+ });
161
+ let res = await send();
162
+ if (res.status === 401 && held) {
163
+ held = void 0;
164
+ res = await send();
165
+ }
166
+ if (!res.ok) await fail(res, `${method} ${path}`);
167
+ return res.status === 204 ? void 0 : res.json();
168
+ }
169
+ const data = async (method, path, body) => (await call(method, path, body))?.data;
170
+ const id = encodeURIComponent;
171
+ return {
172
+ token,
173
+ request: data,
174
+ organizations: {
175
+ async list(query = {}) {
176
+ const q = new URLSearchParams();
177
+ for (const [k, v] of Object.entries(query)) if (v !== void 0) q.set(k, String(v));
178
+ const out2 = await call("GET", `/v1/organizations${q.size ? `?${q}` : ""}`);
179
+ return { items: out2.data.map(toOrganization), total: out2.total_count };
180
+ },
181
+ async create(input) {
182
+ return toOrganization(await data(
183
+ "POST",
184
+ "/v1/organizations",
185
+ { name: input.name, kind: input.kind, external_id: input.externalId, metadata: input.metadata }
186
+ ));
187
+ },
188
+ async get(orgId) {
189
+ return toOrganization(await data("GET", `/v1/organizations/${id(orgId)}`));
190
+ },
191
+ async update(orgId, patch) {
192
+ return toOrganization(await data("PATCH", `/v1/organizations/${id(orgId)}`, patch));
193
+ },
194
+ async remove(orgId) {
195
+ await call("DELETE", `/v1/organizations/${id(orgId)}`);
196
+ }
197
+ },
198
+ members: {
199
+ async list(orgId) {
200
+ return (await data("GET", `/v1/organizations/${id(orgId)}/members`)).map(toMember);
201
+ },
202
+ async add(orgId, m) {
203
+ return toMember(await data(
204
+ "POST",
205
+ `/v1/organizations/${id(orgId)}/members`,
206
+ { role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
207
+ ));
208
+ },
209
+ async setRole(orgId, userId, role) {
210
+ return toMember(await data("PUT", `/v1/organizations/${id(orgId)}/members/${id(userId)}`, { role }));
211
+ },
212
+ async remove(orgId, userId) {
213
+ await call("DELETE", `/v1/organizations/${id(orgId)}/members/${id(userId)}`);
214
+ }
215
+ },
216
+ async invite(person) {
217
+ const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
218
+ return { id: u.id, email: u.email };
219
+ },
220
+ async publicJwks() {
221
+ if (!key) return { keys: [] };
222
+ const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
223
+ return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
224
+ }
225
+ };
226
+ }
227
+
57
228
  // src/webhook.ts
58
229
  import { createRemoteJWKSet, jwtVerify } from "jose";
59
230
  var CauthyWebhookError = class extends Error {
@@ -75,8 +246,8 @@ function keysOf(issuer) {
75
246
  }
76
247
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
77
248
  async function verifyWebhook(request, options = {}) {
78
- const env2 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
79
- const issuer = (options.issuer ?? env2 ?? "").replace(/\/+$/, "");
249
+ const env3 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
250
+ const issuer = (options.issuer ?? env3 ?? "").replace(/\/+$/, "");
80
251
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
81
252
  const signature = request.headers.get("x-cauthy-signature-jwt");
82
253
  if (!signature) throw new CauthyWebhookError("no signature");
@@ -125,20 +296,12 @@ async function verifyWebhook(request, options = {}) {
125
296
  };
126
297
  }
127
298
 
128
- // src/types.ts
129
- var CauthyConfigError = class extends Error {
130
- constructor(message) {
131
- super(`cauthy-auth: ${message}`);
132
- this.name = "CauthyConfigError";
133
- }
134
- };
135
-
136
299
  // src/cauthy.ts
137
300
  var SESSION_DAYS = 30;
138
301
  var FLOW_SECONDS = 600;
139
302
  var SKEW_SECONDS = 30;
140
303
  var ORGANIZATIONS_TTL_MS = 6e4;
141
- function env(name) {
304
+ function env2(name) {
142
305
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
143
306
  }
144
307
  function b64url(bytes) {
@@ -165,11 +328,11 @@ function redirect(location, cookies = []) {
165
328
  return new Response(null, { status: 302, headers: h });
166
329
  }
167
330
  function createCauthy(given = {}) {
168
- const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
169
- const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
170
- const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET");
171
- const secret = given.secret ?? env("CAUTHY_SECRET") ?? "";
172
- const system = given.system ?? env("CAUTHY_SYSTEM") ?? "";
331
+ const issuer = (given.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
332
+ const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
333
+ const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
334
+ const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
335
+ const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
173
336
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
174
337
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
175
338
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
@@ -438,6 +601,7 @@ function createCauthy(given = {}) {
438
601
  return {
439
602
  handler,
440
603
  webhook,
604
+ system: createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch }),
441
605
  getSession,
442
606
  status,
443
607
  resolve: resolve2,
@@ -650,11 +814,11 @@ function toWire(theme) {
650
814
  return wire;
651
815
  }
652
816
  async function pushTheme(o) {
653
- const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
654
- const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
655
- const clientId = o.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
656
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
657
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
817
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
818
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
819
+ const clientId = o.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
820
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
821
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
658
822
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
659
823
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
660
824
  }
@@ -683,11 +847,11 @@ function can(organization, resource, action) {
683
847
  return organization.permissions.some((p) => matches(p, resource, action));
684
848
  }
685
849
  async function pushPolicy(o) {
686
- const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
687
- const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
688
- const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
689
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
690
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
850
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
851
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
852
+ const system = o.system ?? env3("CAUTHY_SYSTEM") ?? "";
853
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
854
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
691
855
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
692
856
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
693
857
  }
@@ -701,11 +865,58 @@ async function pushPolicy(o) {
701
865
  throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
702
866
  }
703
867
  }
868
+
869
+ // src/connect.ts
870
+ function sessionSecret() {
871
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
872
+ let s = "";
873
+ for (const b of bytes) s += String.fromCharCode(b);
874
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
875
+ }
876
+ async function connectSystem(o) {
877
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
878
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
879
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
880
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
881
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
882
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
883
+ }
884
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/connect`, {
885
+ method: "POST",
886
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
887
+ body: JSON.stringify({
888
+ system: o.system,
889
+ app_url: o.appUrl,
890
+ name: o.name,
891
+ callback_path: o.callbackPath,
892
+ events_url: o.eventsUrl,
893
+ events: o.events,
894
+ jwks_uri: o.jwksUri
895
+ })
896
+ });
897
+ const body = await res.json().catch(() => null);
898
+ if (!res.ok || !body?.data) {
899
+ const why = (body?.errors ?? []).map((e) => e.message).filter(Boolean).join("; ") || res.statusText;
900
+ throw new Error(`cauthy-auth: Cauthy did not connect the system (${res.status}): ${why}`);
901
+ }
902
+ const d = body.data;
903
+ return {
904
+ system: d.system,
905
+ clientId: d.client.client_id,
906
+ clientSecret: d.client_secret,
907
+ redirectUris: d.client.redirect_uris,
908
+ webhook: d.webhook,
909
+ env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
910
+ };
911
+ }
704
912
  export {
913
+ CauthyApiError,
705
914
  CauthyConfigError,
706
915
  CauthyWebhookError,
707
916
  can,
917
+ connectSystem,
708
918
  createCauthy,
919
+ createSystem,
709
920
  pushPolicy,
710
921
  pushTheme,
711
922
  themeFromStackmyth,