@cauthyhq/node 1.2.1 → 1.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/CHANGELOG.md +21 -0
- package/README.md +13 -27
- package/dist/{cauthy-CCSSj3Lg.d.mts → cauthy-CsWUgyv_.d.mts} +42 -145
- package/dist/{cauthy-C7gZ-pbv.d.ts → cauthy-DHLBEuqP.d.ts} +42 -145
- package/dist/express.d.mts +2 -5
- package/dist/express.d.ts +2 -5
- package/dist/express.js +1 -2
- package/dist/express.js.map +1 -1
- package/dist/express.mjs +1 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +1 -3
- package/dist/fastify.d.ts +1 -3
- package/dist/fastify.js +0 -2
- package/dist/fastify.js.map +1 -1
- package/dist/fastify.mjs +0 -2
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +2 -5
- package/dist/hono.d.ts +2 -5
- package/dist/hono.js +1 -2
- package/dist/hono.js.map +1 -1
- package/dist/hono.mjs +1 -2
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +1 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +169 -524
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +162 -514
- package/dist/index.mjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.mjs.map +1 -1
- package/dist/next.d.mts +1 -3
- package/dist/next.d.ts +1 -3
- package/dist/testing.d.mts +11 -5
- package/dist/testing.d.ts +11 -5
- package/dist/testing.js +52 -50
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +52 -50
- package/dist/testing.mjs.map +1 -1
- package/package.json +4 -15
- package/dist/identity-BTJ8NB_w.d.mts +0 -27
- package/dist/identity-BTJ8NB_w.d.ts +0 -27
- package/dist/keys-D5k0omTb.d.mts +0 -64
- package/dist/keys-D5k0omTb.d.ts +0 -64
- package/dist/keys.d.mts +0 -8
- package/dist/keys.d.ts +0 -8
- package/dist/keys.js +0 -157
- package/dist/keys.js.map +0 -1
- package/dist/keys.mjs +0 -129
- package/dist/keys.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -64,261 +64,6 @@ async function unsealIndexed(sealed, ring) {
|
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// src/types.ts
|
|
68
|
-
var CauthyConfigError = class extends Error {
|
|
69
|
-
constructor(message) {
|
|
70
|
-
super(`@cauthyhq/node: ${message}`);
|
|
71
|
-
this.name = "CauthyConfigError";
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
var CauthyKeyError = class extends CauthyConfigError {
|
|
75
|
-
constructor(reason, message) {
|
|
76
|
-
super(`CAUTHY_KEY: ${message} (${reason})`);
|
|
77
|
-
this.reason = reason;
|
|
78
|
-
this.name = "CauthyKeyError";
|
|
79
|
-
}
|
|
80
|
-
reason;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// src/url.ts
|
|
84
|
-
function withoutTrailingSlashes(s) {
|
|
85
|
-
let end = s.length;
|
|
86
|
-
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
|
87
|
-
return s.slice(0, end);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// src/contract.ts
|
|
91
|
-
var STANDARD_VARIABLES = [
|
|
92
|
-
"CAUTHY_ISSUER",
|
|
93
|
-
"CAUTHY_CLIENT_ID",
|
|
94
|
-
"CAUTHY_CLIENT_SECRET",
|
|
95
|
-
"CAUTHY_SECRET",
|
|
96
|
-
"CAUTHY_SYSTEM",
|
|
97
|
-
"CAUTHY_TENANT",
|
|
98
|
-
"CAUTHY_SYSTEM_KEY",
|
|
99
|
-
"CAUTHY_SYSTEM_KEY_ID"
|
|
100
|
-
];
|
|
101
|
-
function failClosed(environment) {
|
|
102
|
-
const set = STANDARD_VARIABLES.filter((name) => (environment?.[name] ?? "") !== "");
|
|
103
|
-
if (set.length > 0) {
|
|
104
|
-
throw new CauthyConfigError(`the application runs by contract, and ${set.join(", ")} ${set.length === 1 ? "is" : "are"} also set: remove ` + (set.length === 1 ? "it" : "them") + " from the environment. Everything comes from the contract.");
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
var SLUG = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
|
|
108
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
109
|
-
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
110
|
-
var text = (v) => typeof v === "string" ? v : "";
|
|
111
|
-
function httpsAddress(raw, what) {
|
|
112
|
-
let u;
|
|
113
|
-
try {
|
|
114
|
-
u = new URL(raw);
|
|
115
|
-
} catch {
|
|
116
|
-
throw new CauthyConfigError(`the contract's ${what} is not an address: ${JSON.stringify(raw)}`);
|
|
117
|
-
}
|
|
118
|
-
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
119
|
-
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
120
|
-
throw new CauthyConfigError(`the contract's ${what} must be https: ${raw}`);
|
|
121
|
-
}
|
|
122
|
-
return u;
|
|
123
|
-
}
|
|
124
|
-
function readContract(options) {
|
|
125
|
-
const { system } = options;
|
|
126
|
-
const provider = options.provider ?? "cauthy";
|
|
127
|
-
if (!SLUG.test(system ?? "")) throw new CauthyConfigError(`contract.system must be the system's id in the graph, e.g. "acme-crm"`);
|
|
128
|
-
const graph = options.graph && typeof options.graph === "object" && "default" in options.graph && !("edges" in options.graph) ? options.graph.default : options.graph;
|
|
129
|
-
if (!graph || !Array.isArray(graph.edges) || !Array.isArray(graph.nodes)) {
|
|
130
|
-
throw new CauthyConfigError("contract.graph is not a context graph: pass the graph.json that `cauthy contracts pull` writes");
|
|
131
|
-
}
|
|
132
|
-
const node = (id) => graph.nodes?.find((n) => n.type === "system" && n.id === id);
|
|
133
|
-
const own = node(system);
|
|
134
|
-
if (!own) throw new CauthyConfigError(`the graph has no system "${system}"`);
|
|
135
|
-
const cauthy = node(provider);
|
|
136
|
-
if (!cauthy) throw new CauthyConfigError(`the graph has no system "${provider}"`);
|
|
137
|
-
const links = graph.edges.filter((e2) => e2.relation === "links" && e2.from === system && e2.to === provider && e2.sign_in).filter((e2) => options.link === void 0 || e2.link === options.link);
|
|
138
|
-
if (links.length === 0) {
|
|
139
|
-
throw new CauthyConfigError(options.link ? `the graph has no link "${options.link}" from ${system} to ${provider} with sign_in` : `no link of the graph signs ${system}'s people in with ${provider}: merge one in the contracts repository, then pull the graph again`);
|
|
140
|
-
}
|
|
141
|
-
if (links.length > 1) {
|
|
142
|
-
throw new CauthyConfigError(`${system} has ${links.length} links to ${provider}: name the one to read (contract.link): ${links.map((l) => text(l.link)).join(", ")}`);
|
|
143
|
-
}
|
|
144
|
-
const e = links[0];
|
|
145
|
-
const link = text(e.link);
|
|
146
|
-
const named = text(e.identity?.tenant);
|
|
147
|
-
const pinned = text(e.identity?.tenant_id);
|
|
148
|
-
if (named === NIL_UUID || pinned === NIL_UUID) throw new CauthyConfigError(`the link ${link} names the nil UUID as its workspace: name the workspace by its slug`);
|
|
149
|
-
let tenantId;
|
|
150
|
-
let tenant;
|
|
151
|
-
if (UUID.test(named)) {
|
|
152
|
-
if (pinned !== "" && pinned.toLowerCase() !== named.toLowerCase()) {
|
|
153
|
-
throw new CauthyConfigError(`the link ${link} names the workspace ${named} and its tenant_id ${pinned} does not match: pull the graph again`);
|
|
154
|
-
}
|
|
155
|
-
tenantId = named;
|
|
156
|
-
tenant = text((cauthy.tenants ?? []).find((t) => t.id === named)?.slug);
|
|
157
|
-
if (tenant === "") throw new CauthyConfigError(`the graph does not name the slug of the workspace ${named} that the link ${link} uses`);
|
|
158
|
-
} else {
|
|
159
|
-
if (!SLUG.test(named)) throw new CauthyConfigError(`the link ${link} names no workspace: identity.tenant is a UUID or a slug`);
|
|
160
|
-
if (!UUID.test(pinned)) {
|
|
161
|
-
throw new CauthyConfigError(e.identity?.create_tenant === true ? `the link ${link} asks Cauthy for a new workspace "${named}", and the graph has no tenant_id for it yet: once Cauthy has created it and core-contracts lists it, pull the graph again` : `the link ${link} names the workspace "${named}" by its slug, and the graph carries no tenant_id for it: pull the graph again`);
|
|
162
|
-
}
|
|
163
|
-
tenantId = pinned;
|
|
164
|
-
tenant = named;
|
|
165
|
-
}
|
|
166
|
-
const issuer = withoutTrailingSlashes(httpsAddress(text(cauthy.url), `address of ${provider}`).href);
|
|
167
|
-
const signIn = e.sign_in ?? {};
|
|
168
|
-
const clientId = text(signIn.client_id) || `pact.${link}`;
|
|
169
|
-
const redirectUris = Array.isArray(signIn.redirect_uris) ? signIn.redirect_uris.map(text).filter(Boolean) : [];
|
|
170
|
-
if (redirectUris.length === 0) throw new CauthyConfigError(`the link ${link} names no redirect URI`);
|
|
171
|
-
const proof = text(signIn.system_proof);
|
|
172
|
-
if (signIn.system_proof !== void 0 && proof !== "off" && proof !== "auto" && proof !== "required") {
|
|
173
|
-
throw new CauthyConfigError(`the link ${link} has system_proof ${JSON.stringify(signIn.system_proof)}: it is off, auto or required. Update @cauthyhq/node, or pull the graph again`);
|
|
174
|
-
}
|
|
175
|
-
const systemProof = proof === "off" || proof === "auto" || proof === "required" ? proof : signIn.require_system_proof === true ? "required" : "off";
|
|
176
|
-
if (systemProof !== "off" && !e.service) {
|
|
177
|
-
throw new CauthyConfigError(`the link ${link} asks for the system's proof (${systemProof}) and gives ${system} no identity to make it with: it needs a service block`);
|
|
178
|
-
}
|
|
179
|
-
const out2 = {
|
|
180
|
-
system,
|
|
181
|
-
link,
|
|
182
|
-
issuer,
|
|
183
|
-
tenantId,
|
|
184
|
-
tenant,
|
|
185
|
-
clientId,
|
|
186
|
-
redirectUris,
|
|
187
|
-
requireSystemProof: systemProof === "required",
|
|
188
|
-
systemProof,
|
|
189
|
-
requireVerifiedEmail: signIn.require_verified_email === true,
|
|
190
|
-
persistentSessions: signIn.persistent_sessions === true
|
|
191
|
-
};
|
|
192
|
-
if (e.service) {
|
|
193
|
-
out2.serviceClientId = text(e.service.client_id) || `pact.${link}.service`;
|
|
194
|
-
const jwks = text(e.service.jwks_url) || text(own.jwks_url);
|
|
195
|
-
if (jwks === "") throw new CauthyConfigError(`the link ${link} gives ${system} its own identity, and ${system} names no jwks_url`);
|
|
196
|
-
out2.jwksUrl = httpsAddress(jwks, "jwks_url").href;
|
|
197
|
-
out2.jwksPath = new URL(out2.jwksUrl).pathname;
|
|
198
|
-
}
|
|
199
|
-
if (text(own.events_url) !== "") out2.eventsUrl = httpsAddress(text(own.events_url), "events_url").href;
|
|
200
|
-
return out2;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// src/keys.ts
|
|
204
|
-
import { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK } from "jose";
|
|
205
|
-
var RETIRE_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
206
|
-
var FORGET_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
207
|
-
var CURRENT_TTL_MS = 5 * 60 * 1e3;
|
|
208
|
-
var JWKS_MAX_AGE_S = 300;
|
|
209
|
-
var SIGN_AFTER_MS = (JWKS_MAX_AGE_S + 60) * 1e3;
|
|
210
|
-
var SETTLING_TTL_MS = 5 * 1e3;
|
|
211
|
-
var newestFirst = (keys2) => [...keys2].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.kid.localeCompare(a.kid));
|
|
212
|
-
function published(key) {
|
|
213
|
-
const { kty, n, e } = key.publicJwk;
|
|
214
|
-
return { kty, n, e, kid: key.kid, alg: "RS256", use: "sig" };
|
|
215
|
-
}
|
|
216
|
-
function pactKeys(store, sealKeys, now = Date.now) {
|
|
217
|
-
let remembered;
|
|
218
|
-
let making;
|
|
219
|
-
async function make(after) {
|
|
220
|
-
const { publicKey, privateKey } = await generateKeyPair("RS256", { modulusLength: 2048, extractable: true });
|
|
221
|
-
const publicJwk = await exportJWK(publicKey);
|
|
222
|
-
const kid = await calculateJwkThumbprint(publicJwk);
|
|
223
|
-
const at = Math.max(now(), after ? Date.parse(after.createdAt) + 1 : 0);
|
|
224
|
-
const key = { kid, publicJwk, sealed: await seal(await exportJWK(privateKey), (await sealKeys)[0]), createdAt: new Date(at).toISOString() };
|
|
225
|
-
if (!after && store.addFirst) await store.addFirst(key);
|
|
226
|
-
else await store.add(key);
|
|
227
|
-
return key;
|
|
228
|
-
}
|
|
229
|
-
const age = (key) => now() - Date.parse(key.createdAt);
|
|
230
|
-
function signer(kept) {
|
|
231
|
-
const ripe = kept.find((k) => age(k) >= SIGN_AFTER_MS);
|
|
232
|
-
if (!ripe) return { key: kept[kept.length - 1], until: now() + SETTLING_TTL_MS };
|
|
233
|
-
const until = ripe === kept[0] ? now() + CURRENT_TTL_MS : Math.min(now() + CURRENT_TTL_MS, Date.parse(kept[0].createdAt) + SIGN_AFTER_MS);
|
|
234
|
-
return { key: ripe, until };
|
|
235
|
-
}
|
|
236
|
-
async function open(key) {
|
|
237
|
-
const ring = await sealKeys;
|
|
238
|
-
for (let i = 0; i < ring.length; i++) {
|
|
239
|
-
const jwk = await unseal(key.sealed, ring[i]);
|
|
240
|
-
if (jwk !== null) return { jwk, fresh: i === 0 };
|
|
241
|
-
}
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
async function keptKeys() {
|
|
245
|
-
const kept = newestFirst(await store.list());
|
|
246
|
-
if (kept.length > 0) return kept;
|
|
247
|
-
making ??= make().finally(() => {
|
|
248
|
-
making = void 0;
|
|
249
|
-
});
|
|
250
|
-
await making;
|
|
251
|
-
const again = newestFirst(await store.list());
|
|
252
|
-
if (again.length === 0) throw new CauthyConfigError("the key store kept no key: check that it writes");
|
|
253
|
-
return again;
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
async current() {
|
|
257
|
-
if (remembered && remembered.until > now()) return remembered;
|
|
258
|
-
const { key, until } = signer(await keptKeys());
|
|
259
|
-
if (remembered?.kid === key.kid) {
|
|
260
|
-
remembered.until = until;
|
|
261
|
-
return remembered;
|
|
262
|
-
}
|
|
263
|
-
const opened = await open(key);
|
|
264
|
-
if (opened === null) {
|
|
265
|
-
throw new CauthyConfigError(`the key ${key.kid} cannot be opened with the root key (PACT_ROOT_KEY): if the root key was rotated, set the previous one as PACT_ROOT_KEY_PREVIOUS`);
|
|
266
|
-
}
|
|
267
|
-
remembered = { kid: key.kid, privateKey: await importJWK(opened.jwk, "RS256"), until };
|
|
268
|
-
return remembered;
|
|
269
|
-
},
|
|
270
|
-
async published() {
|
|
271
|
-
const kept = await keptKeys();
|
|
272
|
-
const out2 = [];
|
|
273
|
-
for (let i = 0; i < kept.length; i++) {
|
|
274
|
-
if (i === 0 || now() - Date.parse(kept[i - 1].createdAt) < RETIRE_AFTER_MS) out2.push(published(kept[i]));
|
|
275
|
-
else if (now() - Date.parse(kept[i - 1].createdAt) > FORGET_AFTER_MS) await store.remove(kept[i].kid);
|
|
276
|
-
}
|
|
277
|
-
return { keys: out2 };
|
|
278
|
-
},
|
|
279
|
-
async rotate() {
|
|
280
|
-
const key = await make(newestFirst(await store.list())[0]);
|
|
281
|
-
remembered = void 0;
|
|
282
|
-
return key.kid;
|
|
283
|
-
},
|
|
284
|
-
async reseal() {
|
|
285
|
-
let changed = 0;
|
|
286
|
-
const ring = await sealKeys;
|
|
287
|
-
for (const key of await store.list()) {
|
|
288
|
-
const opened = await open(key);
|
|
289
|
-
if (opened === null || opened.fresh) continue;
|
|
290
|
-
await store.update({ ...key, sealed: await seal(opened.jwk, ring[0]) });
|
|
291
|
-
changed++;
|
|
292
|
-
}
|
|
293
|
-
return changed;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// src/rootkey.ts
|
|
299
|
-
var encoder2 = new TextEncoder();
|
|
300
|
-
var MIN_ROOT_KEY = 32;
|
|
301
|
-
async function derive(root, purpose) {
|
|
302
|
-
const ikm = await crypto.subtle.importKey("raw", encoder2.encode(root), "HKDF", false, ["deriveBits"]);
|
|
303
|
-
const bits = await crypto.subtle.deriveBits(
|
|
304
|
-
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: encoder2.encode(`pact/${purpose}/v1`) },
|
|
305
|
-
ikm,
|
|
306
|
-
256
|
|
307
|
-
);
|
|
308
|
-
return new Uint8Array(bits);
|
|
309
|
-
}
|
|
310
|
-
function deriveKeys(root, previous) {
|
|
311
|
-
if (root.length < MIN_ROOT_KEY) {
|
|
312
|
-
throw new CauthyConfigError(`rootKey (PACT_ROOT_KEY) must be at least ${MIN_ROOT_KEY} characters: 32 random bytes, e.g. openssl rand -base64 32`);
|
|
313
|
-
}
|
|
314
|
-
if (previous !== void 0 && previous !== "" && previous.length < MIN_ROOT_KEY) {
|
|
315
|
-
throw new CauthyConfigError(`the previous root key (PACT_ROOT_KEY_PREVIOUS) must be at least ${MIN_ROOT_KEY} characters`);
|
|
316
|
-
}
|
|
317
|
-
const roots = previous && previous !== root ? [root, previous] : [root];
|
|
318
|
-
const ring = (purpose) => Promise.all(roots.map((r) => derive(r, purpose)));
|
|
319
|
-
return { seal: ring("seal"), cookie: ring("cookie"), flow: ring("flow") };
|
|
320
|
-
}
|
|
321
|
-
|
|
322
67
|
// src/deadline.ts
|
|
323
68
|
var DEFAULT_DEADLINE_MS = 5e3;
|
|
324
69
|
var isTimeout = (err) => err?.name === "TimeoutError";
|
|
@@ -352,6 +97,13 @@ function anySignal(signals) {
|
|
|
352
97
|
return controller.signal;
|
|
353
98
|
}
|
|
354
99
|
|
|
100
|
+
// src/url.ts
|
|
101
|
+
function withoutTrailingSlashes(s) {
|
|
102
|
+
let end = s.length;
|
|
103
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
|
104
|
+
return s.slice(0, end);
|
|
105
|
+
}
|
|
106
|
+
|
|
355
107
|
// src/permissions.ts
|
|
356
108
|
function matches(entry, resource, action) {
|
|
357
109
|
const i = entry.lastIndexOf(":");
|
|
@@ -421,19 +173,19 @@ var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
|
421
173
|
function policySource(system, read) {
|
|
422
174
|
const roles = Array.isArray(read) ? read : read.roles;
|
|
423
175
|
const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
|
|
424
|
-
const
|
|
425
|
-
const key = (k) => IDENT.test(k) ? k :
|
|
176
|
+
const text = (v) => JSON.stringify(v);
|
|
177
|
+
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
426
178
|
const one = (r, pad) => {
|
|
427
|
-
const parts = [`role: ${
|
|
428
|
-
if (r.name) parts.push(`name: ${
|
|
429
|
-
if (r.description) parts.push(`description: ${
|
|
430
|
-
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(
|
|
179
|
+
const parts = [`role: ${text(r.role)}`];
|
|
180
|
+
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
181
|
+
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
182
|
+
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
431
183
|
if (r.permissions?.length) {
|
|
432
184
|
const list = r.permissions.map((p) => `${pad} { ${[
|
|
433
|
-
...p.kind && p.kind !== "*" ? [`${key("kind")}: ${
|
|
434
|
-
`resource: ${
|
|
435
|
-
`action: ${
|
|
436
|
-
...p.condition ? [`condition: ${
|
|
185
|
+
...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [],
|
|
186
|
+
`resource: ${text(p.resource)}`,
|
|
187
|
+
`action: ${text(p.action)}`,
|
|
188
|
+
...p.condition ? [`condition: ${text(p.condition)}`] : []
|
|
437
189
|
].join(", ")} },`);
|
|
438
190
|
parts.push(`permissions: [
|
|
439
191
|
${list.join("\n")}
|
|
@@ -445,14 +197,14 @@ ${pad} `)},
|
|
|
445
197
|
${pad} },`;
|
|
446
198
|
};
|
|
447
199
|
const resource = (c) => {
|
|
448
|
-
const parts = [`resource: ${
|
|
449
|
-
if (c.description) parts.push(`description: ${
|
|
450
|
-
parts.push(`actions: [${c.actions.map(
|
|
200
|
+
const parts = [`resource: ${text(c.resource)}`];
|
|
201
|
+
if (c.description) parts.push(`description: ${text(c.description)}`);
|
|
202
|
+
parts.push(`actions: [${c.actions.map(text).join(", ")}]`);
|
|
451
203
|
const attrs = Object.entries(c.attributes ?? {});
|
|
452
|
-
if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${
|
|
204
|
+
if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${text(v)}`).join(", ")} }`);
|
|
453
205
|
return ` { ${parts.join(", ")} },`;
|
|
454
206
|
};
|
|
455
|
-
const head = `// The catalog of roles of ${
|
|
207
|
+
const head = `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
456
208
|
// Written by \`pullPolicy\`: run it again after the catalog
|
|
457
209
|
// changes, and do not edit this file by hand.
|
|
458
210
|
`;
|
|
@@ -562,7 +314,7 @@ function organizationIdOf(o) {
|
|
|
562
314
|
}
|
|
563
315
|
|
|
564
316
|
// src/system.ts
|
|
565
|
-
import { exportJWK
|
|
317
|
+
import { exportJWK, importJWK, importPKCS8, SignJWT } from "jose";
|
|
566
318
|
|
|
567
319
|
// src/theme.ts
|
|
568
320
|
var MAX_RADIUS = 40;
|
|
@@ -579,7 +331,7 @@ function withoutComments(css) {
|
|
|
579
331
|
}
|
|
580
332
|
}
|
|
581
333
|
function declarations(css) {
|
|
582
|
-
const
|
|
334
|
+
const text = withoutComments(css);
|
|
583
335
|
const out2 = [];
|
|
584
336
|
const within = [];
|
|
585
337
|
let chunk = "";
|
|
@@ -590,7 +342,7 @@ function declarations(css) {
|
|
|
590
342
|
if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
|
|
591
343
|
chunk = "";
|
|
592
344
|
};
|
|
593
|
-
for (const ch of
|
|
345
|
+
for (const ch of text) {
|
|
594
346
|
if (quote) {
|
|
595
347
|
chunk += ch;
|
|
596
348
|
if (ch === quote) quote = "";
|
|
@@ -926,6 +678,22 @@ async function pushTheme(o) {
|
|
|
926
678
|
}
|
|
927
679
|
}
|
|
928
680
|
|
|
681
|
+
// src/types.ts
|
|
682
|
+
var CauthyConfigError = class extends Error {
|
|
683
|
+
constructor(message) {
|
|
684
|
+
super(`@cauthyhq/node: ${message}`);
|
|
685
|
+
this.name = "CauthyConfigError";
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
var CauthyKeyError = class extends CauthyConfigError {
|
|
689
|
+
constructor(reason, message) {
|
|
690
|
+
super(`CAUTHY_KEY: ${message} (${reason})`);
|
|
691
|
+
this.reason = reason;
|
|
692
|
+
this.name = "CauthyKeyError";
|
|
693
|
+
}
|
|
694
|
+
reason;
|
|
695
|
+
};
|
|
696
|
+
|
|
929
697
|
// src/system.ts
|
|
930
698
|
var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
931
699
|
var SKEW_MS = 3e4;
|
|
@@ -965,7 +733,7 @@ function asJwk(k) {
|
|
|
965
733
|
}
|
|
966
734
|
async function loadKey(k) {
|
|
967
735
|
const jwk = asJwk(k);
|
|
968
|
-
return jwk ?
|
|
736
|
+
return jwk ? importJWK(jwk, "RS256") : importPKCS8(k.privateKey, "RS256", { extractable: true });
|
|
969
737
|
}
|
|
970
738
|
function createSystem(given = {}) {
|
|
971
739
|
const issuer = withoutTrailingSlashes(given.issuer ?? env("CAUTHY_ISSUER") ?? "");
|
|
@@ -992,10 +760,6 @@ function createSystem(given = {}) {
|
|
|
992
760
|
if (given.signer) return;
|
|
993
761
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
|
|
994
762
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
995
|
-
if (given.keys) {
|
|
996
|
-
if (!given.keysClientId) throw new CauthyConfigError("the contract gives the system no identity of its own: its link needs a service block");
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
763
|
if (key ? key.keyId === "" : clientSecret === "") {
|
|
1000
764
|
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)");
|
|
1001
765
|
}
|
|
@@ -1013,14 +777,14 @@ function createSystem(given = {}) {
|
|
|
1013
777
|
let asking;
|
|
1014
778
|
async function ask() {
|
|
1015
779
|
configured();
|
|
1016
|
-
const self = given.signer ? given.signer.clientId :
|
|
780
|
+
const self = given.signer ? given.signer.clientId : key?.clientId || clientId;
|
|
1017
781
|
const form = new URLSearchParams({ grant_type: "client_credentials", client_id: self });
|
|
1018
782
|
if (given.signer) {
|
|
1019
783
|
const s = await given.signer.signer;
|
|
1020
784
|
form.set("client_assertion_type", ASSERTION_TYPE);
|
|
1021
785
|
form.set("client_assertion", await s.assertion(self, tokenEndpoint));
|
|
1022
|
-
} else if (
|
|
1023
|
-
const signer =
|
|
786
|
+
} else if (key) {
|
|
787
|
+
const signer = { kid: key.keyId, privateKey: await signingKey() };
|
|
1024
788
|
const assertion = await new SignJWT({}).setProtectedHeader({ alg: "RS256", kid: signer.kid }).setIssuer(self).setSubject(self).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(signer.privateKey);
|
|
1025
789
|
form.set("client_assertion_type", ASSERTION_TYPE);
|
|
1026
790
|
form.set("client_assertion", assertion);
|
|
@@ -1129,9 +893,8 @@ function createSystem(given = {}) {
|
|
|
1129
893
|
return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
|
|
1130
894
|
},
|
|
1131
895
|
async publicJwks() {
|
|
1132
|
-
if (given.keys) return given.keys.published();
|
|
1133
896
|
if (!key) return { keys: [] };
|
|
1134
|
-
const { kty, n, e } = asJwk(key) ?? await
|
|
897
|
+
const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
|
|
1135
898
|
return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
|
|
1136
899
|
}
|
|
1137
900
|
};
|
|
@@ -1465,8 +1228,8 @@ var fail = (reason, message) => {
|
|
|
1465
1228
|
throw new CauthyKeyError(reason, message);
|
|
1466
1229
|
};
|
|
1467
1230
|
var NOT_A_KEY = "not a Cauthy key (copy it again from the dashboard)";
|
|
1468
|
-
function parseKey(
|
|
1469
|
-
const s =
|
|
1231
|
+
function parseKey(text) {
|
|
1232
|
+
const s = text.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, "");
|
|
1470
1233
|
if (s === "" || s.length > MAX_LEN || !printableAscii(s)) fail("key_format", NOT_A_KEY);
|
|
1471
1234
|
const head = s.split(".", 1)[0];
|
|
1472
1235
|
if (head !== KEY_PREFIX) {
|
|
@@ -1649,10 +1412,10 @@ async function finish(res, route, cacheable = false) {
|
|
|
1649
1412
|
if (!cacheable && !(headers.get("cache-control") ?? "").includes("no-store")) headers.set("cache-control", "no-store");
|
|
1650
1413
|
let body = res.body;
|
|
1651
1414
|
if (route !== null && SIGN_IN_ROUTES.has(route) && res.ok && (headers.get("content-type") ?? "").startsWith("application/json")) {
|
|
1652
|
-
const
|
|
1653
|
-
body =
|
|
1415
|
+
const text = await res.text();
|
|
1416
|
+
body = text;
|
|
1654
1417
|
try {
|
|
1655
|
-
const parsed = JSON.parse(
|
|
1418
|
+
const parsed = JSON.parse(text);
|
|
1656
1419
|
const next = nextOf(route, parsed?.status);
|
|
1657
1420
|
if (parsed !== null && next !== void 0) body = JSON.stringify({ ...parsed, next });
|
|
1658
1421
|
} catch {
|
|
@@ -1668,24 +1431,24 @@ import { compactVerify, createLocalJWKSet } from "jose";
|
|
|
1668
1431
|
// src/json.ts
|
|
1669
1432
|
var SPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
|
|
1670
1433
|
var LIMIT = 2n ** 53n;
|
|
1671
|
-
function parseStrict(
|
|
1434
|
+
function parseStrict(text, integerMembers = []) {
|
|
1672
1435
|
let i = 0;
|
|
1673
1436
|
let depth = 0;
|
|
1674
1437
|
const fail2 = (what) => {
|
|
1675
1438
|
throw new SyntaxError(`${what} at ${i}`);
|
|
1676
1439
|
};
|
|
1677
1440
|
const skip = () => {
|
|
1678
|
-
while (i <
|
|
1441
|
+
while (i < text.length && SPACE.has(text[i])) i++;
|
|
1679
1442
|
};
|
|
1680
1443
|
const value = (integer = false) => {
|
|
1681
1444
|
skip();
|
|
1682
|
-
const c =
|
|
1445
|
+
const c = text[i];
|
|
1683
1446
|
if (c === "{") return object();
|
|
1684
1447
|
if (c === "[") return array();
|
|
1685
1448
|
if (c === '"') return string();
|
|
1686
1449
|
if (c === "-" || c >= "0" && c <= "9") return number(integer);
|
|
1687
1450
|
for (const [word, v] of [["true", true], ["false", false], ["null", null]]) {
|
|
1688
|
-
if (
|
|
1451
|
+
if (text.startsWith(word, i)) {
|
|
1689
1452
|
i += word.length;
|
|
1690
1453
|
return v;
|
|
1691
1454
|
}
|
|
@@ -1695,13 +1458,13 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1695
1458
|
const string = () => {
|
|
1696
1459
|
const start = i;
|
|
1697
1460
|
i++;
|
|
1698
|
-
while (i <
|
|
1699
|
-
if (
|
|
1461
|
+
while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1;
|
|
1462
|
+
if (text[i] !== '"') fail2("unterminated string");
|
|
1700
1463
|
i++;
|
|
1701
|
-
return JSON.parse(
|
|
1464
|
+
return JSON.parse(text.slice(start, i));
|
|
1702
1465
|
};
|
|
1703
1466
|
const number = (integer) => {
|
|
1704
|
-
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(
|
|
1467
|
+
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(i));
|
|
1705
1468
|
if (!m) return fail2("bad number");
|
|
1706
1469
|
i += m[0].length;
|
|
1707
1470
|
if (integer && (m[2] !== void 0 || m[3] !== void 0)) return Number.NaN;
|
|
@@ -1716,7 +1479,7 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1716
1479
|
depth++;
|
|
1717
1480
|
const out2 = [];
|
|
1718
1481
|
skip();
|
|
1719
|
-
if (
|
|
1482
|
+
if (text[i] === "]") {
|
|
1720
1483
|
i++;
|
|
1721
1484
|
depth--;
|
|
1722
1485
|
return out2;
|
|
@@ -1724,8 +1487,8 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1724
1487
|
for (; ; ) {
|
|
1725
1488
|
out2.push(value());
|
|
1726
1489
|
skip();
|
|
1727
|
-
if (
|
|
1728
|
-
else if (
|
|
1490
|
+
if (text[i] === ",") i++;
|
|
1491
|
+
else if (text[i] === "]") {
|
|
1729
1492
|
i++;
|
|
1730
1493
|
depth--;
|
|
1731
1494
|
return out2;
|
|
@@ -1738,24 +1501,24 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1738
1501
|
const out2 = {};
|
|
1739
1502
|
const seen = /* @__PURE__ */ new Set();
|
|
1740
1503
|
skip();
|
|
1741
|
-
if (
|
|
1504
|
+
if (text[i] === "}") {
|
|
1742
1505
|
i++;
|
|
1743
1506
|
depth--;
|
|
1744
1507
|
return out2;
|
|
1745
1508
|
}
|
|
1746
1509
|
for (; ; ) {
|
|
1747
1510
|
skip();
|
|
1748
|
-
if (
|
|
1511
|
+
if (text[i] !== '"') fail2("expected a name");
|
|
1749
1512
|
const name = string();
|
|
1750
1513
|
if (seen.has(name)) fail2(`repeated name ${JSON.stringify(name)}`);
|
|
1751
1514
|
seen.add(name);
|
|
1752
1515
|
skip();
|
|
1753
|
-
if (
|
|
1516
|
+
if (text[i] !== ":") fail2("expected :");
|
|
1754
1517
|
i++;
|
|
1755
1518
|
Object.defineProperty(out2, name, { value: value(depth === 1 && integerMembers.includes(name)), enumerable: true, writable: true, configurable: true });
|
|
1756
1519
|
skip();
|
|
1757
|
-
if (
|
|
1758
|
-
else if (
|
|
1520
|
+
if (text[i] === ",") i++;
|
|
1521
|
+
else if (text[i] === "}") {
|
|
1759
1522
|
i++;
|
|
1760
1523
|
depth--;
|
|
1761
1524
|
return out2;
|
|
@@ -1764,7 +1527,7 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1764
1527
|
};
|
|
1765
1528
|
const result = value();
|
|
1766
1529
|
skip();
|
|
1767
|
-
if (i <
|
|
1530
|
+
if (i < text.length) fail2("trailing characters");
|
|
1768
1531
|
return result;
|
|
1769
1532
|
}
|
|
1770
1533
|
|
|
@@ -1785,6 +1548,15 @@ function deprecated(what, instead) {
|
|
|
1785
1548
|
var enc2 = new TextEncoder();
|
|
1786
1549
|
var PKCS8_ED25519 = Uint8Array.from([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]);
|
|
1787
1550
|
var ASSERTION_TYPE2 = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
1551
|
+
async function ringKey(secret, purpose) {
|
|
1552
|
+
const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
|
|
1553
|
+
return new Uint8Array(await crypto.subtle.deriveBits(
|
|
1554
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: enc2.encode(`pact/${purpose}/v1`) },
|
|
1555
|
+
ikm,
|
|
1556
|
+
256
|
|
1557
|
+
));
|
|
1558
|
+
}
|
|
1559
|
+
var ringOf = (secrets, purpose) => Promise.all(secrets.map((s) => ringKey(s, purpose)));
|
|
1788
1560
|
async function seedOf(secret) {
|
|
1789
1561
|
const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
|
|
1790
1562
|
return new Uint8Array(await crypto.subtle.deriveBits(
|
|
@@ -1822,12 +1594,11 @@ async function signerOf(secret) {
|
|
|
1822
1594
|
}
|
|
1823
1595
|
function deriveFromKey(key, previous) {
|
|
1824
1596
|
const other = previous && previous.secret !== key.secret ? previous : void 0;
|
|
1825
|
-
const
|
|
1597
|
+
const secrets = other ? [key.secret, other.secret] : [key.secret];
|
|
1826
1598
|
return {
|
|
1827
|
-
cookie:
|
|
1828
|
-
flow:
|
|
1829
|
-
signer: signerOf(key.secret)
|
|
1830
|
-
...other ? { previousJwk: signerOf(other.secret).then((s) => ({ ...s.publicJwk, kid: s.kid })) } : {}
|
|
1599
|
+
cookie: ringOf(secrets, "cookie"),
|
|
1600
|
+
flow: ringOf(secrets, "flow"),
|
|
1601
|
+
signer: signerOf(key.secret)
|
|
1831
1602
|
};
|
|
1832
1603
|
}
|
|
1833
1604
|
|
|
@@ -1946,8 +1717,8 @@ var CauthyTokenError = class extends Error {
|
|
|
1946
1717
|
detail;
|
|
1947
1718
|
};
|
|
1948
1719
|
var MAX_TOKEN_BYTES = 8192;
|
|
1949
|
-
var
|
|
1950
|
-
var
|
|
1720
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1721
|
+
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
1951
1722
|
var SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
1952
1723
|
var MAX_KEYS_BYTES = 64 * 1024;
|
|
1953
1724
|
var KEYS_TIMEOUT_MS = 5e3;
|
|
@@ -2027,7 +1798,7 @@ async function readCapped(res, max) {
|
|
|
2027
1798
|
}
|
|
2028
1799
|
return out2;
|
|
2029
1800
|
}
|
|
2030
|
-
var
|
|
1801
|
+
var published = /* @__PURE__ */ new Map();
|
|
2031
1802
|
var withFetch = /* @__PURE__ */ new WeakMap();
|
|
2032
1803
|
function remoteKeysFor(issuer, fetchKeys) {
|
|
2033
1804
|
let byIssuer = withFetch.get(fetchKeys);
|
|
@@ -2037,10 +1808,10 @@ function remoteKeysFor(issuer, fetchKeys) {
|
|
|
2037
1808
|
return set;
|
|
2038
1809
|
}
|
|
2039
1810
|
function keysOf2(issuer) {
|
|
2040
|
-
let set =
|
|
1811
|
+
let set = published.get(issuer);
|
|
2041
1812
|
if (set === void 0) {
|
|
2042
1813
|
set = remoteKeys(issuer);
|
|
2043
|
-
|
|
1814
|
+
published.set(issuer, set);
|
|
2044
1815
|
}
|
|
2045
1816
|
return set;
|
|
2046
1817
|
}
|
|
@@ -2082,7 +1853,7 @@ function createTokenVerifier(options = {}) {
|
|
|
2082
1853
|
if (audience === "") throw new CauthyConfigError("audience (CAUTHY_AUDIENCE) is not set: without it a token issued for another application would be accepted");
|
|
2083
1854
|
const tenant = options.tenantId ?? env2("CAUTHY_TENANT_ID") ?? "";
|
|
2084
1855
|
if (tenant === "") throw new CauthyConfigError("tenantId (CAUTHY_TENANT_ID) is not set: without it a token of another workspace would be accepted");
|
|
2085
|
-
if (!
|
|
1856
|
+
if (!UUID.test(tenant) || tenant === NIL_UUID) throw new CauthyConfigError("tenantId must be the workspace's id, a lowercase UUID that is not the nil UUID");
|
|
2086
1857
|
const leeway = options.leeway ?? 30;
|
|
2087
1858
|
if (!Number.isFinite(leeway) || leeway < 0 || leeway > 60) throw new CauthyConfigError("leeway must be 0 to 60 seconds");
|
|
2088
1859
|
return async (from) => {
|
|
@@ -2131,8 +1902,8 @@ function createTokenVerifier(options = {}) {
|
|
|
2131
1902
|
throw new CauthyTokenError("invalid_claim", name);
|
|
2132
1903
|
};
|
|
2133
1904
|
if (typeof claims.iss !== "string") bad("iss");
|
|
2134
|
-
if (typeof claims.sub !== "string" || !
|
|
2135
|
-
if (typeof claims.tid !== "string" || !
|
|
1905
|
+
if (typeof claims.sub !== "string" || !UUID.test(claims.sub)) bad("sub");
|
|
1906
|
+
if (typeof claims.tid !== "string" || !UUID.test(claims.tid)) bad("tid");
|
|
2136
1907
|
const aud = claims.aud;
|
|
2137
1908
|
if (!(typeof aud === "string" || Array.isArray(aud) && aud.length > 0 && aud.every((a) => typeof a === "string"))) bad("aud");
|
|
2138
1909
|
for (const name of ["exp", "iat", "nbf"]) if (claims[name] !== void 0 && !Number.isInteger(claims[name])) bad(name);
|
|
@@ -2170,13 +1941,13 @@ async function verifyToken(from, options = {}) {
|
|
|
2170
1941
|
return createTokenVerifier(options)(from);
|
|
2171
1942
|
}
|
|
2172
1943
|
async function createVerifierFromEnv(options = {}) {
|
|
2173
|
-
const
|
|
2174
|
-
if (
|
|
1944
|
+
const text = options.key ?? env2("CAUTHY_KEY");
|
|
1945
|
+
if (text === void 0 || text === "") return createTokenVerifier(options);
|
|
2175
1946
|
refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
|
|
2176
1947
|
for (const name of ["issuer", "audience", "tenantId"]) {
|
|
2177
1948
|
if (options[name] !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
|
|
2178
1949
|
}
|
|
2179
|
-
const key = parseKey(
|
|
1950
|
+
const key = parseKey(text);
|
|
2180
1951
|
const system = createSystem({ issuer: key.issuer, clientId: key.clientId, signer: { clientId: key.clientId, signer: deriveFromKey(key).signer }, fetch: options.fetch });
|
|
2181
1952
|
const id = await createIdentity({ issuer: key.issuer, token: () => system.token(), fetch: deadline(options.fetch, KEYS_TIMEOUT_MS) }).ready();
|
|
2182
1953
|
const { key: _, ...rest } = options;
|
|
@@ -2273,9 +2044,18 @@ function redirect(location, cookies = []) {
|
|
|
2273
2044
|
function conditionalOf(c) {
|
|
2274
2045
|
return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
|
|
2275
2046
|
}
|
|
2047
|
+
function refuseContracts(given) {
|
|
2048
|
+
const options = given;
|
|
2049
|
+
const set = [
|
|
2050
|
+
...["contract", "keys", "rootKey", "rootKeyPrevious"].filter((name) => options[name] !== void 0).map((name) => `the ${name} option`),
|
|
2051
|
+
...["PACT_ROOT_KEY", "PACT_ROOT_KEY_PREVIOUS"].filter((name) => (env3(name) ?? "") !== "")
|
|
2052
|
+
];
|
|
2053
|
+
if (set.length === 0) return;
|
|
2054
|
+
throw new CauthyConfigError(`contracts are retired: run on CAUTHY_KEY alone, and remove ${set.join(", ")}. An application without a key makes it on its card in Cauthy's dashboard; the same key keeps its people signed in. Protect sign-in with the "Protect sign-in" switches on that card. Sessions sealed with PACT_ROOT_KEY are no longer read.`);
|
|
2055
|
+
}
|
|
2276
2056
|
function byKey(given) {
|
|
2277
|
-
const
|
|
2278
|
-
if (
|
|
2057
|
+
const text = given.key ?? env3("CAUTHY_KEY");
|
|
2058
|
+
if (text === void 0 || text === "") return void 0;
|
|
2279
2059
|
refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
|
|
2280
2060
|
for (const [name, value] of [
|
|
2281
2061
|
["issuer", given.issuer],
|
|
@@ -2286,12 +2066,11 @@ function byKey(given) {
|
|
|
2286
2066
|
["tenant", given.tenant],
|
|
2287
2067
|
["audience", given.audience],
|
|
2288
2068
|
["tenantId", given.tenantId],
|
|
2289
|
-
["systemKey", given.systemKey]
|
|
2290
|
-
["rootKey", given.rootKey]
|
|
2069
|
+
["systemKey", given.systemKey]
|
|
2291
2070
|
]) {
|
|
2292
2071
|
if (value !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
|
|
2293
2072
|
}
|
|
2294
|
-
const key = parseKey(
|
|
2073
|
+
const key = parseKey(text);
|
|
2295
2074
|
const prevText = given.keyPrevious ?? env3("CAUTHY_KEY_PREVIOUS");
|
|
2296
2075
|
let previous;
|
|
2297
2076
|
if (prevText) {
|
|
@@ -2306,14 +2085,8 @@ function byKey(given) {
|
|
|
2306
2085
|
throw new CauthyKeyError("key_previous_mismatch", "CAUTHY_KEY_PREVIOUS names another Cauthy or client than CAUTHY_KEY");
|
|
2307
2086
|
}
|
|
2308
2087
|
const legacySecret = env3("CAUTHY_SECRET") || void 0;
|
|
2309
|
-
const legacyRoot = env3("PACT_ROOT_KEY") || void 0;
|
|
2310
|
-
const legacyRootPrevious = legacyRoot ? env3("PACT_ROOT_KEY_PREVIOUS") || void 0 : void 0;
|
|
2311
2088
|
if (legacySecret) deprecated("CAUTHY_SECRET beside CAUTHY_KEY", "It is read only for sessions made before the move: remove it after the rotation window.");
|
|
2312
|
-
|
|
2313
|
-
if (given.keys && !(legacyRoot && given.contract)) {
|
|
2314
|
-
throw new CauthyConfigError("with CAUTHY_KEY the application keeps no key store: remove keys (it is read only beside PACT_ROOT_KEY and a contract, while moving)");
|
|
2315
|
-
}
|
|
2316
|
-
return { key, previous, legacySecret, legacyRoot, legacyRootPrevious };
|
|
2089
|
+
return { key, previous, legacySecret };
|
|
2317
2090
|
}
|
|
2318
2091
|
var UnknownOriginError = class extends Error {
|
|
2319
2092
|
constructor(origin, registered) {
|
|
@@ -2324,78 +2097,41 @@ var UnknownOriginError = class extends Error {
|
|
|
2324
2097
|
origin;
|
|
2325
2098
|
registered;
|
|
2326
2099
|
};
|
|
2327
|
-
function contractOnKey(given, keyed) {
|
|
2328
|
-
const contract = readContract(given.contract);
|
|
2329
|
-
if (contract.issuer !== keyed.key.issuer) {
|
|
2330
|
-
throw new CauthyKeyError("key_contract_issuer", `the key names ${keyed.key.issuer}, and the contract's Cauthy is ${contract.issuer}: make the key in that Cauthy's dashboard`);
|
|
2331
|
-
}
|
|
2332
|
-
return contract;
|
|
2333
|
-
}
|
|
2334
|
-
function byContract(given) {
|
|
2335
|
-
failClosed(typeof process !== "undefined" ? process.env : void 0);
|
|
2336
|
-
for (const [name, value] of [
|
|
2337
|
-
["clientId", given.clientId],
|
|
2338
|
-
["clientSecret", given.clientSecret],
|
|
2339
|
-
["secret", given.secret],
|
|
2340
|
-
["system", given.system],
|
|
2341
|
-
["tenant", given.tenant],
|
|
2342
|
-
["systemKey", given.systemKey]
|
|
2343
|
-
]) {
|
|
2344
|
-
if (value !== void 0) throw new CauthyConfigError(`with a contract, ${name} comes from the contract: do not pass it`);
|
|
2345
|
-
}
|
|
2346
|
-
const contract = readContract(given.contract);
|
|
2347
|
-
const root = given.rootKey ?? env3("PACT_ROOT_KEY") ?? "";
|
|
2348
|
-
if (root === "") throw new CauthyConfigError("rootKey (PACT_ROOT_KEY) is required with a contract: 32 random bytes, e.g. openssl rand -base64 32");
|
|
2349
|
-
if (contract.serviceClientId && !given.keys) {
|
|
2350
|
-
throw new CauthyConfigError(`the link ${contract.link} gives ${contract.system} its own identity: pass keys, where its key is kept (@cauthyhq/node/keys)`);
|
|
2351
|
-
}
|
|
2352
|
-
deprecated("PACT_ROOT_KEY", "Make the application's CAUTHY_KEY in Cauthy's dashboard (the application's card), then remove PACT_ROOT_KEY, the keys option and its table.");
|
|
2353
|
-
return { contract, root, previous: given.rootKeyPrevious ?? env3("PACT_ROOT_KEY_PREVIOUS") };
|
|
2354
|
-
}
|
|
2355
2100
|
var toldOfHeader = false;
|
|
2356
2101
|
var toldOfUnopened = false;
|
|
2357
2102
|
function toldUnopened() {
|
|
2358
2103
|
if (toldOfUnopened) return;
|
|
2359
2104
|
toldOfUnopened = true;
|
|
2360
|
-
console.warn("@cauthyhq/node: a session cookie arrived that no key of this application opens; it was cleared and the person is signed out. Moving to CAUTHY_KEY, keep the secret that sealed the old sessions (CAUTHY_SECRET
|
|
2105
|
+
console.warn("@cauthyhq/node: a session cookie arrived that no key of this application opens; it was cleared and the person is signed out. Moving to CAUTHY_KEY, keep the secret that sealed the old sessions (CAUTHY_SECRET) beside it for the rotation window; rotating the key, set CAUTHY_KEY_PREVIOUS.");
|
|
2361
2106
|
}
|
|
2362
2107
|
function createCauthy(given = {}) {
|
|
2108
|
+
refuseContracts(given);
|
|
2363
2109
|
const keyed = byKey(given);
|
|
2364
|
-
const
|
|
2365
|
-
const
|
|
2366
|
-
const
|
|
2367
|
-
const
|
|
2368
|
-
const
|
|
2369
|
-
const
|
|
2370
|
-
const askSelf = keyed !== void 0 && contract === void 0;
|
|
2371
|
-
const system = askSelf ? "" : contract?.system ?? given.system ?? env3("CAUTHY_SYSTEM") ?? "";
|
|
2372
|
-
const tenant = askSelf ? "" : contract?.tenant ?? given.tenant ?? env3("CAUTHY_TENANT") ?? "";
|
|
2110
|
+
const issuer = withoutTrailingSlashes(keyed?.key.issuer ?? given.issuer ?? env3("CAUTHY_ISSUER") ?? "");
|
|
2111
|
+
const clientId = keyed?.key.clientId ?? given.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
|
|
2112
|
+
const clientSecret = keyed ? void 0 : given.clientSecret ?? env3("CAUTHY_CLIENT_SECRET");
|
|
2113
|
+
const secret = keyed ? "" : given.secret ?? env3("CAUTHY_SECRET") ?? "";
|
|
2114
|
+
const system = keyed ? "" : given.system ?? env3("CAUTHY_SYSTEM") ?? "";
|
|
2115
|
+
const tenant = keyed ? "" : given.tenant ?? env3("CAUTHY_TENANT") ?? "";
|
|
2373
2116
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
|
|
2374
2117
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
2375
|
-
if (!
|
|
2118
|
+
if (!keyed && secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
|
|
2376
2119
|
const fromKey = keyed ? deriveFromKey(keyed.key, keyed.previous) : void 0;
|
|
2377
2120
|
let signingKid;
|
|
2378
2121
|
fromKey?.signer.then((s) => {
|
|
2379
2122
|
signingKid = s.kid;
|
|
2380
2123
|
}, () => void 0);
|
|
2381
|
-
const derived = pacted ? deriveKeys(pacted.root, pacted.previous) : void 0;
|
|
2382
|
-
const single = derived || fromKey ? void 0 : keyOf(secret).then((k) => [k]);
|
|
2383
|
-
const legacyRings = keyed?.legacyRoot ? deriveKeys(keyed.legacyRoot, keyed.legacyRootPrevious) : void 0;
|
|
2384
2124
|
const legacySingle = keyed?.legacySecret ? keyOf(keyed.legacySecret) : void 0;
|
|
2385
|
-
const withLegacy = async (own
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
const cookieKeys = fromKey ? withLegacy(fromKey.cookie, "cookie") : derived?.cookie ?? single;
|
|
2391
|
-
const flowKeys = fromKey ? withLegacy(fromKey.flow, "flow") : derived?.flow ?? single;
|
|
2392
|
-
if (!keyed && !pacted) {
|
|
2125
|
+
const withLegacy = async (own) => [...await own, ...legacySingle ? [await legacySingle] : []];
|
|
2126
|
+
const single = fromKey ? Promise.resolve([]) : keyOf(secret).then((k) => [k]);
|
|
2127
|
+
const cookieKeys = fromKey ? withLegacy(fromKey.cookie) : single;
|
|
2128
|
+
const flowKeys = fromKey ? withLegacy(fromKey.flow) : single;
|
|
2129
|
+
if (!keyed) {
|
|
2393
2130
|
deprecated(
|
|
2394
2131
|
"Configuring with CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_CLIENT_SECRET, CAUTHY_SECRET and the other CAUTHY_* variables (or their options)",
|
|
2395
2132
|
"Connect the application in Cauthy's dashboard and use the one variable CAUTHY_KEY."
|
|
2396
2133
|
);
|
|
2397
2134
|
}
|
|
2398
|
-
const ownKeys = derived && given.keys ? pactKeys(given.keys, derived.seal) : legacyRings && contract && given.keys ? pactKeys(given.keys, legacyRings.seal) : void 0;
|
|
2399
2135
|
const baseURL = given.baseURL ?? (keyed ? void 0 : env3("CAUTHY_BASE_URL"));
|
|
2400
2136
|
if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
|
|
2401
2137
|
throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
|
|
@@ -2432,21 +2168,10 @@ function createCauthy(given = {}) {
|
|
|
2432
2168
|
acceptInviteUrl,
|
|
2433
2169
|
fetch: given.fetch,
|
|
2434
2170
|
timeoutMs: systemTimeout,
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
...fromKey ? {
|
|
2438
|
-
signer: { clientId: contract ? contract.serviceClientId ?? keyed.key.clientId : clientId, signer: fromKey.signer },
|
|
2439
|
-
currentSystem: () => identity?.current()?.system ?? ""
|
|
2440
|
-
} : {}
|
|
2171
|
+
// On CAUTHY_KEY the key proves the key's own client.
|
|
2172
|
+
...fromKey ? { signer: { clientId, signer: fromKey.signer }, currentSystem: () => identity?.current()?.system ?? "" } : {}
|
|
2441
2173
|
});
|
|
2442
|
-
const
|
|
2443
|
-
issuer,
|
|
2444
|
-
token: () => ownSystem.token(),
|
|
2445
|
-
fetch: deadline(given.fetch, requestTimeout),
|
|
2446
|
-
kid: () => signingKid,
|
|
2447
|
-
log: (m) => console.error(`@cauthyhq/node: ${m}`)
|
|
2448
|
-
});
|
|
2449
|
-
const identity = askSelf ? createIdentity({
|
|
2174
|
+
const identity = keyed ? createIdentity({
|
|
2450
2175
|
issuer,
|
|
2451
2176
|
token: () => ownSystem.token(),
|
|
2452
2177
|
fetch: deadline(given.fetch, requestTimeout),
|
|
@@ -2456,6 +2181,8 @@ function createCauthy(given = {}) {
|
|
|
2456
2181
|
identity?.ready().catch(() => {
|
|
2457
2182
|
});
|
|
2458
2183
|
const tenantNow = () => identity?.current()?.tenant.slug ?? tenant;
|
|
2184
|
+
const signInClientNow = () => identity?.current()?.sign_in_client_id || clientId;
|
|
2185
|
+
const signInClient = async () => identity ? (await identity.ready()).sign_in_client_id || clientId : clientId;
|
|
2459
2186
|
const systemNow = () => identity?.current()?.system ?? system;
|
|
2460
2187
|
const requestOrigin = (request) => {
|
|
2461
2188
|
const url = new URL(request.url);
|
|
@@ -2480,7 +2207,7 @@ function createCauthy(given = {}) {
|
|
|
2480
2207
|
};
|
|
2481
2208
|
const attrs = (request, maxAge) => ({ secure: safeOrigin(request).startsWith("https://"), maxAge });
|
|
2482
2209
|
async function clientAuth(form) {
|
|
2483
|
-
if (fromKey &&
|
|
2210
|
+
if (fromKey && form.get("client_id") === clientId) {
|
|
2484
2211
|
const s = await fromKey.signer;
|
|
2485
2212
|
form.set("client_assertion_type", ASSERTION_TYPE2);
|
|
2486
2213
|
form.set("client_assertion", await s.assertion(clientId, `${issuer}/v1/oauth/token`));
|
|
@@ -2490,8 +2217,8 @@ function createCauthy(given = {}) {
|
|
|
2490
2217
|
return form;
|
|
2491
2218
|
}
|
|
2492
2219
|
async function token(form, proof = {}) {
|
|
2493
|
-
const body2 = await clientAuth(new URLSearchParams({ client_id: clientId, ...form }));
|
|
2494
2220
|
try {
|
|
2221
|
+
const body2 = await clientAuth(new URLSearchParams({ client_id: await signInClient(), ...form }));
|
|
2495
2222
|
const res = await doFetch(`${issuer}/v1/oauth/token`, {
|
|
2496
2223
|
method: "POST",
|
|
2497
2224
|
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json", ...proof },
|
|
@@ -2529,18 +2256,16 @@ function createCauthy(given = {}) {
|
|
|
2529
2256
|
}
|
|
2530
2257
|
const kindOf = (body2, was) => (typeof body2?.persistent === "boolean" ? body2.persistent : was?.p === 1) ? { p: 1 } : {};
|
|
2531
2258
|
async function revoke(refreshToken) {
|
|
2532
|
-
|
|
2533
|
-
|
|
2259
|
+
try {
|
|
2260
|
+
const form = await clientAuth(new URLSearchParams({ client_id: await signInClient(), token: refreshToken }));
|
|
2261
|
+
await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
|
|
2262
|
+
} catch {
|
|
2263
|
+
}
|
|
2534
2264
|
}
|
|
2535
|
-
const unverified = (u) => contract?.requireVerifiedEmail === true && !u.emailVerified;
|
|
2536
2265
|
async function stored(body2, was) {
|
|
2537
2266
|
if (typeof body2?.access_token !== "string" || typeof body2?.refresh_token !== "string") return null;
|
|
2538
2267
|
const who = await userinfo(body2.access_token);
|
|
2539
2268
|
if (who === null || who.u.id === "") return null;
|
|
2540
|
-
if (unverified(who.u)) {
|
|
2541
|
-
await revoke(body2.refresh_token);
|
|
2542
|
-
throw new CauthyApiError(403, "email_not_confirmed", "Confirm your email address first.");
|
|
2543
|
-
}
|
|
2544
2269
|
const lifetime = typeof body2.expires_in === "number" && body2.expires_in > 0 ? body2.expires_in : 300;
|
|
2545
2270
|
return { at: body2.access_token, rt: body2.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who, ...kindOf(body2, was) };
|
|
2546
2271
|
}
|
|
@@ -2568,13 +2293,7 @@ function createCauthy(given = {}) {
|
|
|
2568
2293
|
running = (async () => {
|
|
2569
2294
|
const res = await token({ grant_type: "refresh_token", refresh_token: rt });
|
|
2570
2295
|
if (!res.ok) return res.ended ? "ended" : "unreachable";
|
|
2571
|
-
|
|
2572
|
-
try {
|
|
2573
|
-
next = await stored(res.body, was);
|
|
2574
|
-
} catch (err) {
|
|
2575
|
-
if (err instanceof CauthyApiError && err.code === "email_not_confirmed") return "ended";
|
|
2576
|
-
throw err;
|
|
2577
|
-
}
|
|
2296
|
+
const next = await stored(res.body, was);
|
|
2578
2297
|
if (next !== null) return next;
|
|
2579
2298
|
if (typeof res.body?.access_token !== "string" || typeof res.body?.refresh_token !== "string") return "unreachable";
|
|
2580
2299
|
const lifetime = typeof res.body.expires_in === "number" && res.body.expires_in > 0 ? res.body.expires_in : 300;
|
|
@@ -2596,7 +2315,6 @@ function createCauthy(given = {}) {
|
|
|
2596
2315
|
if (present) toldUnopened();
|
|
2597
2316
|
return { session: null, status: "unauthenticated", cookies: present ? clearParts(cookieName, attrs(request, 0), had) : [] };
|
|
2598
2317
|
}
|
|
2599
|
-
if (unverified(value.u)) return { session: null, status: "unauthenticated", cookies: clearParts(cookieName, attrs(request, 0), had) };
|
|
2600
2318
|
if (fresh(value)) return { session: toSession(value), status: "authenticated", cookies: stale ? await write(request, value, had) : [] };
|
|
2601
2319
|
const renewed = await renew(value);
|
|
2602
2320
|
if (renewed === "ended") {
|
|
@@ -2608,7 +2326,7 @@ function createCauthy(given = {}) {
|
|
|
2608
2326
|
async function getSession(request, responseHeaders) {
|
|
2609
2327
|
if (responseHeaders === void 0) {
|
|
2610
2328
|
const { value } = await read(request);
|
|
2611
|
-
return value !== null && fresh(value)
|
|
2329
|
+
return value !== null && fresh(value) ? toSession(value) : null;
|
|
2612
2330
|
}
|
|
2613
2331
|
const r = await resolve2(request);
|
|
2614
2332
|
for (const c of r.cookies) responseHeaders.append("set-cookie", c);
|
|
@@ -2616,7 +2334,7 @@ function createCauthy(given = {}) {
|
|
|
2616
2334
|
}
|
|
2617
2335
|
async function status(request) {
|
|
2618
2336
|
const { value } = await read(request);
|
|
2619
|
-
if (value === null
|
|
2337
|
+
if (value === null) return "unauthenticated";
|
|
2620
2338
|
return fresh(value) ? "authenticated" : "expired";
|
|
2621
2339
|
}
|
|
2622
2340
|
const orgCache = /* @__PURE__ */ new Map();
|
|
@@ -2703,7 +2421,7 @@ function createCauthy(given = {}) {
|
|
|
2703
2421
|
const flow = { state: random(), verifier: random(48), returnTo: localPath(url.searchParams.get("return_to"), afterSignIn) };
|
|
2704
2422
|
const challenge = b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(flow.verifier))));
|
|
2705
2423
|
const q = new URLSearchParams({
|
|
2706
|
-
client_id:
|
|
2424
|
+
client_id: await signInClient(),
|
|
2707
2425
|
redirect_uri: `${originOf(request)}${basePath}/callback`,
|
|
2708
2426
|
response_type: "code",
|
|
2709
2427
|
state: flow.state,
|
|
@@ -2765,28 +2483,17 @@ function createCauthy(given = {}) {
|
|
|
2765
2483
|
);
|
|
2766
2484
|
};
|
|
2767
2485
|
if (!res.ok && res.code === "email_not_confirmed") return unconfirmed();
|
|
2768
|
-
|
|
2769
|
-
try {
|
|
2770
|
-
s = res.ok ? await stored(res.body) : null;
|
|
2771
|
-
} catch (err) {
|
|
2772
|
-
if (!(err instanceof CauthyApiError && err.code === "email_not_confirmed")) throw err;
|
|
2773
|
-
return unconfirmed();
|
|
2774
|
-
}
|
|
2486
|
+
const s = res.ok ? await stored(res.body) : null;
|
|
2775
2487
|
if (s === null) return failed(request, "exchange_failed");
|
|
2776
2488
|
return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]);
|
|
2777
2489
|
}
|
|
2778
2490
|
async function logout(request) {
|
|
2779
2491
|
const { had, value } = await read(request);
|
|
2780
2492
|
if (value !== null) {
|
|
2781
|
-
|
|
2782
|
-
await doFetch(`${issuer}/v1/oauth/revoke`, {
|
|
2783
|
-
method: "POST",
|
|
2784
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
2785
|
-
body: body2
|
|
2786
|
-
}).catch(() => void 0);
|
|
2493
|
+
await revoke(value.rt);
|
|
2787
2494
|
orgCache.delete(value.at);
|
|
2788
2495
|
}
|
|
2789
|
-
const q = new URLSearchParams({ client_id:
|
|
2496
|
+
const q = new URLSearchParams({ client_id: signInClientNow(), post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
|
|
2790
2497
|
return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
|
|
2791
2498
|
}
|
|
2792
2499
|
const STEP_SECONDS = 600;
|
|
@@ -2804,7 +2511,7 @@ function createCauthy(given = {}) {
|
|
|
2804
2511
|
createdAt: String(d?.created_at ?? ""),
|
|
2805
2512
|
lastUsedAt: d?.last_used_at ? String(d.last_used_at) : null
|
|
2806
2513
|
});
|
|
2807
|
-
const
|
|
2514
|
+
const UUID2 = /^[0-9a-f-]{36}$/i;
|
|
2808
2515
|
const seatOf = (d) => ({
|
|
2809
2516
|
id: String(d?.id ?? ""),
|
|
2810
2517
|
system: String(d?.system ?? ""),
|
|
@@ -2909,10 +2616,10 @@ function createCauthy(given = {}) {
|
|
|
2909
2616
|
...page ? { "x-cauthy-link-to": `${originOf(request)}${page}` } : { "x-cauthy-links": "hosted" },
|
|
2910
2617
|
// Whose request it is: what Cauthy mails goes in this application's
|
|
2911
2618
|
// name, under its logo.
|
|
2912
|
-
...
|
|
2619
|
+
...signInClientNow() ? { "x-cauthy-client-id": signInClientNow() } : {},
|
|
2913
2620
|
...await passedOn(request),
|
|
2914
2621
|
...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
|
|
2915
|
-
// The person asked to stay signed in: Cauthy keeps the session long if the
|
|
2622
|
+
// The person asked to stay signed in: Cauthy keeps the session long if the workspace allows it.
|
|
2916
2623
|
...remember ? { "x-cauthy-remember": "1" } : {},
|
|
2917
2624
|
// The answer to the workspace's Turnstile challenge, from the
|
|
2918
2625
|
// person's browser: Cauthy checks it at the doors that ask for it.
|
|
@@ -3053,7 +2760,7 @@ function createCauthy(given = {}) {
|
|
|
3053
2760
|
}
|
|
3054
2761
|
if (route === "/look" && !post) {
|
|
3055
2762
|
if (lookCache === null || lookCache.until < Date.now()) {
|
|
3056
|
-
const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(
|
|
2763
|
+
const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(signInClientNow())}`);
|
|
3057
2764
|
lookCache = { until: Date.now() + LOOK_TTL_MS, look: res.error ? {} : res.data ?? {} };
|
|
3058
2765
|
}
|
|
3059
2766
|
return json(lookCache.look);
|
|
@@ -3078,7 +2785,7 @@ function createCauthy(given = {}) {
|
|
|
3078
2785
|
signUp: settings.data?.allow_signup !== false && settings.data?.signup?.mode !== "invite_only",
|
|
3079
2786
|
// The application's system makes an organization per sign-up: the form asks the company's name.
|
|
3080
2787
|
...settings.data?.signup?.organization?.ask === true ? { organization: { ask: true, required: settings.data.signup.organization.required === true } } : {},
|
|
3081
|
-
// Whether the person may ask to stay signed in: the
|
|
2788
|
+
// Whether the person may ask to stay signed in: the workspace's setting in Cauthy (persistent_sessions).
|
|
3082
2789
|
...typeof settings.data?.persistent_sessions === "boolean" ? { persistentSessions: settings.data.persistent_sessions } : {},
|
|
3083
2790
|
// A sign-in link needs a page of the application to open.
|
|
3084
2791
|
magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
|
|
@@ -3242,7 +2949,7 @@ function createCauthy(given = {}) {
|
|
|
3242
2949
|
let path = "/v1/auth/me/organizations";
|
|
3243
2950
|
if (route !== "/me/organizations") {
|
|
3244
2951
|
const id = url.searchParams.get("id") ?? "";
|
|
3245
|
-
if (!
|
|
2952
|
+
if (!UUID2.test(id)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3246
2953
|
path += `/${id}/${route.endsWith("/roles") ? "roles" : "members"}`;
|
|
3247
2954
|
} else if (url.searchParams.get("system")) {
|
|
3248
2955
|
path += `?system=${encodeURIComponent(url.searchParams.get("system"))}`;
|
|
@@ -3303,8 +3010,7 @@ function createCauthy(given = {}) {
|
|
|
3303
3010
|
if (route === "/sign-out") {
|
|
3304
3011
|
const { had, value } = await read(request);
|
|
3305
3012
|
if (value !== null) {
|
|
3306
|
-
|
|
3307
|
-
await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
|
|
3013
|
+
await revoke(value.rt);
|
|
3308
3014
|
orgCache.delete(value.at);
|
|
3309
3015
|
}
|
|
3310
3016
|
const h = new Headers();
|
|
@@ -3531,7 +3237,7 @@ function createCauthy(given = {}) {
|
|
|
3531
3237
|
}
|
|
3532
3238
|
if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
|
|
3533
3239
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3534
|
-
if (!
|
|
3240
|
+
if (!UUID2.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3535
3241
|
const path = `/v1/auth/me/organizations/${org}`;
|
|
3536
3242
|
if (route === "/me/organizations/rename") {
|
|
3537
3243
|
const bad = checkOrganizationName(b.name);
|
|
@@ -3548,7 +3254,7 @@ function createCauthy(given = {}) {
|
|
|
3548
3254
|
}
|
|
3549
3255
|
if (route.startsWith("/me/organizations/members/")) {
|
|
3550
3256
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3551
|
-
if (!
|
|
3257
|
+
if (!UUID2.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3552
3258
|
const base = `/v1/auth/me/organizations/${org}/members`;
|
|
3553
3259
|
if (route === "/me/organizations/members/add") {
|
|
3554
3260
|
const bad = checkEmail(b.email);
|
|
@@ -3559,7 +3265,7 @@ function createCauthy(given = {}) {
|
|
|
3559
3265
|
email: String(b.email).trim(),
|
|
3560
3266
|
role,
|
|
3561
3267
|
name: typeof b.name === "string" ? b.name.trim() : "",
|
|
3562
|
-
client_id:
|
|
3268
|
+
client_id: signInClientNow(),
|
|
3563
3269
|
only_new: true,
|
|
3564
3270
|
...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {}
|
|
3565
3271
|
}, pages.acceptInvite, mine.token);
|
|
@@ -3568,7 +3274,7 @@ function createCauthy(given = {}) {
|
|
|
3568
3274
|
return json({ status: "added", member: orgMemberOf(res.data) }, 200, mine.headers);
|
|
3569
3275
|
}
|
|
3570
3276
|
const user = typeof b.userId === "string" ? b.userId : "";
|
|
3571
|
-
if (!
|
|
3277
|
+
if (!UUID2.test(user)) return problem(400, "invalid_member", "Say which member.", void 0, mine.headers);
|
|
3572
3278
|
if (route === "/me/organizations/members/role") {
|
|
3573
3279
|
const role = typeof b.role === "string" ? b.role : "";
|
|
3574
3280
|
if (role === "") return problem(400, "invalid_role", "Choose a role.", "role", mine.headers);
|
|
@@ -3583,7 +3289,7 @@ function createCauthy(given = {}) {
|
|
|
3583
3289
|
return json({ status: "removed", userId: user }, 200, mine.headers);
|
|
3584
3290
|
}
|
|
3585
3291
|
if (route === "/me/organizations/members/invite") {
|
|
3586
|
-
const res = await ask(request, "POST", `${base}/${user}/invite`, { client_id:
|
|
3292
|
+
const res = await ask(request, "POST", `${base}/${user}/invite`, { client_id: signInClientNow(), ...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {} }, pages.acceptInvite, mine.token);
|
|
3587
3293
|
if (res.error) return refused(res);
|
|
3588
3294
|
return json({ status: "sent", userId: user }, 200, mine.headers);
|
|
3589
3295
|
}
|
|
@@ -3701,18 +3407,15 @@ function createCauthy(given = {}) {
|
|
|
3701
3407
|
const url = new URL(request.url);
|
|
3702
3408
|
const inside = url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
|
|
3703
3409
|
const route = inside ? withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/" : null;
|
|
3704
|
-
const published3 = pact !== void 0 && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD");
|
|
3705
3410
|
if (identity && route !== null) {
|
|
3706
3411
|
const unready = await identityGate();
|
|
3707
3412
|
if (unready) return finish(unready, route);
|
|
3708
3413
|
}
|
|
3709
3414
|
try {
|
|
3710
|
-
return finish(await serve(request), route
|
|
3415
|
+
return finish(await serve(request), route);
|
|
3711
3416
|
} catch (err) {
|
|
3712
3417
|
if (err instanceof UnknownOriginError) return finish(problem(400, "unknown_origin", err.message), route);
|
|
3713
|
-
|
|
3714
|
-
const wantsPage = request.method === "GET" && (request.headers.get("sec-fetch-mode") ?? "navigate") === "navigate";
|
|
3715
|
-
return finish(wantsPage ? new Response("Sign-in refused (email_not_confirmed). Confirm your email address first.", { status: 403, headers: { "content-type": "text/plain; charset=utf-8" } }) : problem(403, "email_not_confirmed", "Confirm your email address first."), route);
|
|
3418
|
+
throw err;
|
|
3716
3419
|
}
|
|
3717
3420
|
}
|
|
3718
3421
|
async function identityGate() {
|
|
@@ -3736,7 +3439,6 @@ function createCauthy(given = {}) {
|
|
|
3736
3439
|
}
|
|
3737
3440
|
async function serve(request) {
|
|
3738
3441
|
const url = new URL(request.url);
|
|
3739
|
-
if (pact && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD")) return pact.jwks();
|
|
3740
3442
|
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
|
|
3741
3443
|
const route = withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/";
|
|
3742
3444
|
const get = request.method === "GET" || request.method === "HEAD";
|
|
@@ -3760,7 +3462,7 @@ function createCauthy(given = {}) {
|
|
|
3760
3462
|
return redirect(back, r.cookies);
|
|
3761
3463
|
}
|
|
3762
3464
|
if (route === "/account" && get) {
|
|
3763
|
-
const q = new URLSearchParams({ client_id:
|
|
3465
|
+
const q = new URLSearchParams({ client_id: signInClientNow() });
|
|
3764
3466
|
const back = localPath(url.searchParams.get("return_to"), "");
|
|
3765
3467
|
if (back !== "") q.set("return_to", `${originOf(request)}${back}`);
|
|
3766
3468
|
const locale = url.searchParams.get("locale");
|
|
@@ -3779,7 +3481,7 @@ function createCauthy(given = {}) {
|
|
|
3779
3481
|
const asker = await signedInAs(request);
|
|
3780
3482
|
if ("response" in asker) return asker.response;
|
|
3781
3483
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3782
|
-
if (!
|
|
3484
|
+
if (!UUID2.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
|
|
3783
3485
|
const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
|
|
3784
3486
|
if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
|
|
3785
3487
|
const attributes2 = b.attributes;
|
|
@@ -3802,8 +3504,7 @@ function createCauthy(given = {}) {
|
|
|
3802
3504
|
}
|
|
3803
3505
|
}
|
|
3804
3506
|
async function protect(request) {
|
|
3805
|
-
const
|
|
3806
|
-
const value = read1.value !== null && unverified(read1.value.u) ? null : read1.value;
|
|
3507
|
+
const { value } = await read(request);
|
|
3807
3508
|
if (value !== null && fresh(value)) return { session: toSession(value) };
|
|
3808
3509
|
const url = new URL(request.url);
|
|
3809
3510
|
const state = value === null ? "none" : "expired";
|
|
@@ -3816,85 +3517,36 @@ function createCauthy(given = {}) {
|
|
|
3816
3517
|
return { response: redirect(to) };
|
|
3817
3518
|
}
|
|
3818
3519
|
const replay = given.webhookReplay ?? memoryReplayStore();
|
|
3819
|
-
let lateSelf;
|
|
3820
3520
|
const tokenVerifier = (o = {}) => {
|
|
3821
|
-
if (
|
|
3822
|
-
if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
|
|
3823
|
-
throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
|
|
3824
|
-
}
|
|
3521
|
+
if (identity) {
|
|
3825
3522
|
let built;
|
|
3826
3523
|
return async (from) => {
|
|
3827
|
-
const id = await
|
|
3524
|
+
const id = await identity.ready();
|
|
3828
3525
|
if (built?.from !== id) {
|
|
3829
|
-
built = { from: id, verify: createTokenVerifier({
|
|
3830
|
-
issuer,
|
|
3831
|
-
tenantId: contract?.tenantId ?? id.tenant.id,
|
|
3832
|
-
audience: id.audience,
|
|
3833
|
-
fetch: given.fetch,
|
|
3834
|
-
...contract ? { clientId: contract.clientId } : {},
|
|
3835
|
-
...o
|
|
3836
|
-
}) };
|
|
3526
|
+
built = { from: id, verify: createTokenVerifier({ issuer, tenantId: id.tenant.id, audience: id.audience, fetch: given.fetch, ...o }) };
|
|
3837
3527
|
}
|
|
3838
3528
|
return built.verify(from);
|
|
3839
3529
|
};
|
|
3840
3530
|
}
|
|
3841
|
-
if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
|
|
3842
|
-
throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
|
|
3843
|
-
}
|
|
3844
3531
|
return createTokenVerifier({
|
|
3845
3532
|
issuer,
|
|
3846
|
-
tenantId:
|
|
3533
|
+
tenantId: given.tenantId,
|
|
3847
3534
|
audience: given.audience,
|
|
3848
3535
|
fetch: given.fetch,
|
|
3849
|
-
...contract ? { clientId: contract.clientId } : {},
|
|
3850
3536
|
...o
|
|
3851
3537
|
});
|
|
3852
3538
|
};
|
|
3853
3539
|
const webhook = (request, o = {}) => verifyWebhook(request, { issuer, url: o.url ?? `${originOf(request)}${new URL(request.url).pathname}`, jwks: o.jwks, now: o.now, replay });
|
|
3854
|
-
async function keyJwks() {
|
|
3855
|
-
const s = await fromKey.signer;
|
|
3856
|
-
const keys2 = [{ ...s.publicJwk, kid: s.kid, alg: "EdDSA", use: "sig" }];
|
|
3857
|
-
if (fromKey.previousJwk) keys2.push({ ...await fromKey.previousJwk, alg: "EdDSA", use: "sig" });
|
|
3858
|
-
if (ownKeys) keys2.push(...(await ownKeys.published()).keys);
|
|
3859
|
-
return new Response(JSON.stringify({ keys: keys2 }), {
|
|
3860
|
-
headers: { "content-type": "application/json; charset=utf-8", "cache-control": `public, max-age=${JWKS_MAX_AGE_S}` }
|
|
3861
|
-
});
|
|
3862
|
-
}
|
|
3863
|
-
const noStore = () => {
|
|
3864
|
-
throw new CauthyConfigError("on CAUTHY_KEY the application keeps no key of its own: make a new key on the application's card in Cauthy's dashboard");
|
|
3865
|
-
};
|
|
3866
|
-
const keyOnly = {
|
|
3867
|
-
current: async () => noStore(),
|
|
3868
|
-
published: async () => ({ keys: [] }),
|
|
3869
|
-
rotate: async () => noStore(),
|
|
3870
|
-
reseal: async () => 0
|
|
3871
|
-
};
|
|
3872
|
-
const pact = contract && fromKey ? {
|
|
3873
|
-
contract,
|
|
3874
|
-
keys: ownKeys ?? keyOnly,
|
|
3875
|
-
jwks: keyJwks,
|
|
3876
|
-
status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
|
|
3877
|
-
} : contract && ownKeys ? {
|
|
3878
|
-
contract,
|
|
3879
|
-
keys: ownKeys,
|
|
3880
|
-
async jwks() {
|
|
3881
|
-
return new Response(JSON.stringify(await ownKeys.published()), {
|
|
3882
|
-
headers: { "content-type": "application/json; charset=utf-8", "cache-control": `public, max-age=${JWKS_MAX_AGE_S}` }
|
|
3883
|
-
});
|
|
3884
|
-
},
|
|
3885
|
-
status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
|
|
3886
|
-
} : void 0;
|
|
3887
3540
|
const configured = () => ({
|
|
3888
3541
|
issuer,
|
|
3889
3542
|
audience: given.audience ?? env3("CAUTHY_AUDIENCE") ?? "",
|
|
3890
|
-
tenant: { id:
|
|
3543
|
+
tenant: { id: given.tenantId ?? env3("CAUTHY_TENANT_ID") ?? "", slug: tenant },
|
|
3891
3544
|
system,
|
|
3892
3545
|
client_id: clientId,
|
|
3893
3546
|
sign_in_client_id: clientId,
|
|
3894
3547
|
app_urls: baseURL ? [withoutTrailingSlashes(baseURL)] : [],
|
|
3895
3548
|
callback_path: `${basePath}/callback`,
|
|
3896
|
-
key: null
|
|
3897
|
-
contract: contract ? { link: contract.link, system_proof: contract.systemProof, require_verified_email: contract.requireVerifiedEmail } : null
|
|
3549
|
+
key: null
|
|
3898
3550
|
});
|
|
3899
3551
|
return {
|
|
3900
3552
|
handler,
|
|
@@ -3916,7 +3568,6 @@ function createCauthy(given = {}) {
|
|
|
3916
3568
|
check,
|
|
3917
3569
|
toPublic,
|
|
3918
3570
|
urls,
|
|
3919
|
-
...pact ? { pact } : {},
|
|
3920
3571
|
options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
|
|
3921
3572
|
};
|
|
3922
3573
|
}
|
|
@@ -4000,7 +3651,6 @@ export {
|
|
|
4000
3651
|
KEY_REPLACES,
|
|
4001
3652
|
MIN_PASSWORD,
|
|
4002
3653
|
PROTOCOL_VERSION,
|
|
4003
|
-
STANDARD_VARIABLES,
|
|
4004
3654
|
can,
|
|
4005
3655
|
checkCode,
|
|
4006
3656
|
checkEmail,
|
|
@@ -4016,7 +3666,6 @@ export {
|
|
|
4016
3666
|
definePolicy,
|
|
4017
3667
|
describePasswordPolicy,
|
|
4018
3668
|
explainPasswordProblems,
|
|
4019
|
-
failClosed,
|
|
4020
3669
|
memoryReplayStore,
|
|
4021
3670
|
parseKey,
|
|
4022
3671
|
passwordPolicyOf,
|
|
@@ -4027,7 +3676,6 @@ export {
|
|
|
4027
3676
|
pushCatalog,
|
|
4028
3677
|
pushPolicy,
|
|
4029
3678
|
pushTheme,
|
|
4030
|
-
readContract,
|
|
4031
3679
|
remoteKeys,
|
|
4032
3680
|
themeFromStackmyth,
|
|
4033
3681
|
tokensFromStackmyth,
|