@cauthyhq/node 1.2.0 → 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 +30 -0
- package/README.md +16 -28
- 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 +187 -530
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -520
- 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 +5 -14
- 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
|
@@ -54,266 +54,14 @@ async function unseal(sealed, key) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
async function unsealAny(sealed, ring) {
|
|
57
|
-
|
|
58
|
-
const value = await unseal(sealed, key);
|
|
59
|
-
if (value !== null) return value;
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// src/types.ts
|
|
65
|
-
var CauthyConfigError = class extends Error {
|
|
66
|
-
constructor(message) {
|
|
67
|
-
super(`@cauthyhq/node: ${message}`);
|
|
68
|
-
this.name = "CauthyConfigError";
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
var CauthyKeyError = class extends CauthyConfigError {
|
|
72
|
-
constructor(reason, message) {
|
|
73
|
-
super(`CAUTHY_KEY: ${message} (${reason})`);
|
|
74
|
-
this.reason = reason;
|
|
75
|
-
this.name = "CauthyKeyError";
|
|
76
|
-
}
|
|
77
|
-
reason;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// src/url.ts
|
|
81
|
-
function withoutTrailingSlashes(s) {
|
|
82
|
-
let end = s.length;
|
|
83
|
-
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
|
|
84
|
-
return s.slice(0, end);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// src/contract.ts
|
|
88
|
-
var STANDARD_VARIABLES = [
|
|
89
|
-
"CAUTHY_ISSUER",
|
|
90
|
-
"CAUTHY_CLIENT_ID",
|
|
91
|
-
"CAUTHY_CLIENT_SECRET",
|
|
92
|
-
"CAUTHY_SECRET",
|
|
93
|
-
"CAUTHY_SYSTEM",
|
|
94
|
-
"CAUTHY_TENANT",
|
|
95
|
-
"CAUTHY_SYSTEM_KEY",
|
|
96
|
-
"CAUTHY_SYSTEM_KEY_ID"
|
|
97
|
-
];
|
|
98
|
-
function failClosed(environment) {
|
|
99
|
-
const set = STANDARD_VARIABLES.filter((name) => (environment?.[name] ?? "") !== "");
|
|
100
|
-
if (set.length > 0) {
|
|
101
|
-
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.");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
var SLUG = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
|
|
105
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
106
|
-
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
107
|
-
var text = (v) => typeof v === "string" ? v : "";
|
|
108
|
-
function httpsAddress(raw, what) {
|
|
109
|
-
let u;
|
|
110
|
-
try {
|
|
111
|
-
u = new URL(raw);
|
|
112
|
-
} catch {
|
|
113
|
-
throw new CauthyConfigError(`the contract's ${what} is not an address: ${JSON.stringify(raw)}`);
|
|
114
|
-
}
|
|
115
|
-
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
116
|
-
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
117
|
-
throw new CauthyConfigError(`the contract's ${what} must be https: ${raw}`);
|
|
118
|
-
}
|
|
119
|
-
return u;
|
|
120
|
-
}
|
|
121
|
-
function readContract(options) {
|
|
122
|
-
const { system } = options;
|
|
123
|
-
const provider = options.provider ?? "cauthy";
|
|
124
|
-
if (!SLUG.test(system ?? "")) throw new CauthyConfigError(`contract.system must be the system's id in the graph, e.g. "acme-crm"`);
|
|
125
|
-
const graph = options.graph && typeof options.graph === "object" && "default" in options.graph && !("edges" in options.graph) ? options.graph.default : options.graph;
|
|
126
|
-
if (!graph || !Array.isArray(graph.edges) || !Array.isArray(graph.nodes)) {
|
|
127
|
-
throw new CauthyConfigError("contract.graph is not a context graph: pass the graph.json that `cauthy contracts pull` writes");
|
|
128
|
-
}
|
|
129
|
-
const node = (id) => graph.nodes?.find((n) => n.type === "system" && n.id === id);
|
|
130
|
-
const own = node(system);
|
|
131
|
-
if (!own) throw new CauthyConfigError(`the graph has no system "${system}"`);
|
|
132
|
-
const cauthy = node(provider);
|
|
133
|
-
if (!cauthy) throw new CauthyConfigError(`the graph has no system "${provider}"`);
|
|
134
|
-
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);
|
|
135
|
-
if (links.length === 0) {
|
|
136
|
-
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`);
|
|
137
|
-
}
|
|
138
|
-
if (links.length > 1) {
|
|
139
|
-
throw new CauthyConfigError(`${system} has ${links.length} links to ${provider}: name the one to read (contract.link): ${links.map((l) => text(l.link)).join(", ")}`);
|
|
140
|
-
}
|
|
141
|
-
const e = links[0];
|
|
142
|
-
const link = text(e.link);
|
|
143
|
-
const named = text(e.identity?.tenant);
|
|
144
|
-
const pinned = text(e.identity?.tenant_id);
|
|
145
|
-
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`);
|
|
146
|
-
let tenantId;
|
|
147
|
-
let tenant;
|
|
148
|
-
if (UUID.test(named)) {
|
|
149
|
-
if (pinned !== "" && pinned.toLowerCase() !== named.toLowerCase()) {
|
|
150
|
-
throw new CauthyConfigError(`the link ${link} names the workspace ${named} and its tenant_id ${pinned} does not match: pull the graph again`);
|
|
151
|
-
}
|
|
152
|
-
tenantId = named;
|
|
153
|
-
tenant = text((cauthy.tenants ?? []).find((t) => t.id === named)?.slug);
|
|
154
|
-
if (tenant === "") throw new CauthyConfigError(`the graph does not name the slug of the workspace ${named} that the link ${link} uses`);
|
|
155
|
-
} else {
|
|
156
|
-
if (!SLUG.test(named)) throw new CauthyConfigError(`the link ${link} names no workspace: identity.tenant is a UUID or a slug`);
|
|
157
|
-
if (!UUID.test(pinned)) {
|
|
158
|
-
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`);
|
|
159
|
-
}
|
|
160
|
-
tenantId = pinned;
|
|
161
|
-
tenant = named;
|
|
162
|
-
}
|
|
163
|
-
const issuer = withoutTrailingSlashes(httpsAddress(text(cauthy.url), `address of ${provider}`).href);
|
|
164
|
-
const signIn = e.sign_in ?? {};
|
|
165
|
-
const clientId = text(signIn.client_id) || `pact.${link}`;
|
|
166
|
-
const redirectUris = Array.isArray(signIn.redirect_uris) ? signIn.redirect_uris.map(text).filter(Boolean) : [];
|
|
167
|
-
if (redirectUris.length === 0) throw new CauthyConfigError(`the link ${link} names no redirect URI`);
|
|
168
|
-
const proof = text(signIn.system_proof);
|
|
169
|
-
if (signIn.system_proof !== void 0 && proof !== "off" && proof !== "auto" && proof !== "required") {
|
|
170
|
-
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`);
|
|
171
|
-
}
|
|
172
|
-
const systemProof = proof === "off" || proof === "auto" || proof === "required" ? proof : signIn.require_system_proof === true ? "required" : "off";
|
|
173
|
-
if (systemProof !== "off" && !e.service) {
|
|
174
|
-
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`);
|
|
175
|
-
}
|
|
176
|
-
const out2 = {
|
|
177
|
-
system,
|
|
178
|
-
link,
|
|
179
|
-
issuer,
|
|
180
|
-
tenantId,
|
|
181
|
-
tenant,
|
|
182
|
-
clientId,
|
|
183
|
-
redirectUris,
|
|
184
|
-
requireSystemProof: systemProof === "required",
|
|
185
|
-
systemProof,
|
|
186
|
-
requireVerifiedEmail: signIn.require_verified_email === true,
|
|
187
|
-
persistentSessions: signIn.persistent_sessions === true
|
|
188
|
-
};
|
|
189
|
-
if (e.service) {
|
|
190
|
-
out2.serviceClientId = text(e.service.client_id) || `pact.${link}.service`;
|
|
191
|
-
const jwks = text(e.service.jwks_url) || text(own.jwks_url);
|
|
192
|
-
if (jwks === "") throw new CauthyConfigError(`the link ${link} gives ${system} its own identity, and ${system} names no jwks_url`);
|
|
193
|
-
out2.jwksUrl = httpsAddress(jwks, "jwks_url").href;
|
|
194
|
-
out2.jwksPath = new URL(out2.jwksUrl).pathname;
|
|
195
|
-
}
|
|
196
|
-
if (text(own.events_url) !== "") out2.eventsUrl = httpsAddress(text(own.events_url), "events_url").href;
|
|
197
|
-
return out2;
|
|
57
|
+
return (await unsealIndexed(sealed, ring))?.value ?? null;
|
|
198
58
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
var FORGET_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
204
|
-
var CURRENT_TTL_MS = 5 * 60 * 1e3;
|
|
205
|
-
var JWKS_MAX_AGE_S = 300;
|
|
206
|
-
var SIGN_AFTER_MS = (JWKS_MAX_AGE_S + 60) * 1e3;
|
|
207
|
-
var SETTLING_TTL_MS = 5 * 1e3;
|
|
208
|
-
var newestFirst = (keys2) => [...keys2].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.kid.localeCompare(a.kid));
|
|
209
|
-
function published(key) {
|
|
210
|
-
const { kty, n, e } = key.publicJwk;
|
|
211
|
-
return { kty, n, e, kid: key.kid, alg: "RS256", use: "sig" };
|
|
212
|
-
}
|
|
213
|
-
function pactKeys(store, sealKeys, now = Date.now) {
|
|
214
|
-
let remembered;
|
|
215
|
-
let making;
|
|
216
|
-
async function make(after) {
|
|
217
|
-
const { publicKey, privateKey } = await generateKeyPair("RS256", { modulusLength: 2048, extractable: true });
|
|
218
|
-
const publicJwk = await exportJWK(publicKey);
|
|
219
|
-
const kid = await calculateJwkThumbprint(publicJwk);
|
|
220
|
-
const at = Math.max(now(), after ? Date.parse(after.createdAt) + 1 : 0);
|
|
221
|
-
const key = { kid, publicJwk, sealed: await seal(await exportJWK(privateKey), (await sealKeys)[0]), createdAt: new Date(at).toISOString() };
|
|
222
|
-
if (!after && store.addFirst) await store.addFirst(key);
|
|
223
|
-
else await store.add(key);
|
|
224
|
-
return key;
|
|
225
|
-
}
|
|
226
|
-
const age = (key) => now() - Date.parse(key.createdAt);
|
|
227
|
-
function signer(kept) {
|
|
228
|
-
const ripe = kept.find((k) => age(k) >= SIGN_AFTER_MS);
|
|
229
|
-
if (!ripe) return { key: kept[kept.length - 1], until: now() + SETTLING_TTL_MS };
|
|
230
|
-
const until = ripe === kept[0] ? now() + CURRENT_TTL_MS : Math.min(now() + CURRENT_TTL_MS, Date.parse(kept[0].createdAt) + SIGN_AFTER_MS);
|
|
231
|
-
return { key: ripe, until };
|
|
232
|
-
}
|
|
233
|
-
async function open(key) {
|
|
234
|
-
const ring = await sealKeys;
|
|
235
|
-
for (let i = 0; i < ring.length; i++) {
|
|
236
|
-
const jwk = await unseal(key.sealed, ring[i]);
|
|
237
|
-
if (jwk !== null) return { jwk, fresh: i === 0 };
|
|
238
|
-
}
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
async function keptKeys() {
|
|
242
|
-
const kept = newestFirst(await store.list());
|
|
243
|
-
if (kept.length > 0) return kept;
|
|
244
|
-
making ??= make().finally(() => {
|
|
245
|
-
making = void 0;
|
|
246
|
-
});
|
|
247
|
-
await making;
|
|
248
|
-
const again = newestFirst(await store.list());
|
|
249
|
-
if (again.length === 0) throw new CauthyConfigError("the key store kept no key: check that it writes");
|
|
250
|
-
return again;
|
|
59
|
+
async function unsealIndexed(sealed, ring) {
|
|
60
|
+
for (let index = 0; index < ring.length; index++) {
|
|
61
|
+
const value = await unseal(sealed, ring[index]);
|
|
62
|
+
if (value !== null) return { value, index };
|
|
251
63
|
}
|
|
252
|
-
return
|
|
253
|
-
async current() {
|
|
254
|
-
if (remembered && remembered.until > now()) return remembered;
|
|
255
|
-
const { key, until } = signer(await keptKeys());
|
|
256
|
-
if (remembered?.kid === key.kid) {
|
|
257
|
-
remembered.until = until;
|
|
258
|
-
return remembered;
|
|
259
|
-
}
|
|
260
|
-
const opened = await open(key);
|
|
261
|
-
if (opened === null) {
|
|
262
|
-
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`);
|
|
263
|
-
}
|
|
264
|
-
remembered = { kid: key.kid, privateKey: await importJWK(opened.jwk, "RS256"), until };
|
|
265
|
-
return remembered;
|
|
266
|
-
},
|
|
267
|
-
async published() {
|
|
268
|
-
const kept = await keptKeys();
|
|
269
|
-
const out2 = [];
|
|
270
|
-
for (let i = 0; i < kept.length; i++) {
|
|
271
|
-
if (i === 0 || now() - Date.parse(kept[i - 1].createdAt) < RETIRE_AFTER_MS) out2.push(published(kept[i]));
|
|
272
|
-
else if (now() - Date.parse(kept[i - 1].createdAt) > FORGET_AFTER_MS) await store.remove(kept[i].kid);
|
|
273
|
-
}
|
|
274
|
-
return { keys: out2 };
|
|
275
|
-
},
|
|
276
|
-
async rotate() {
|
|
277
|
-
const key = await make(newestFirst(await store.list())[0]);
|
|
278
|
-
remembered = void 0;
|
|
279
|
-
return key.kid;
|
|
280
|
-
},
|
|
281
|
-
async reseal() {
|
|
282
|
-
let changed = 0;
|
|
283
|
-
const ring = await sealKeys;
|
|
284
|
-
for (const key of await store.list()) {
|
|
285
|
-
const opened = await open(key);
|
|
286
|
-
if (opened === null || opened.fresh) continue;
|
|
287
|
-
await store.update({ ...key, sealed: await seal(opened.jwk, ring[0]) });
|
|
288
|
-
changed++;
|
|
289
|
-
}
|
|
290
|
-
return changed;
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// src/rootkey.ts
|
|
296
|
-
var encoder2 = new TextEncoder();
|
|
297
|
-
var MIN_ROOT_KEY = 32;
|
|
298
|
-
async function derive(root, purpose) {
|
|
299
|
-
const ikm = await crypto.subtle.importKey("raw", encoder2.encode(root), "HKDF", false, ["deriveBits"]);
|
|
300
|
-
const bits = await crypto.subtle.deriveBits(
|
|
301
|
-
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: encoder2.encode(`pact/${purpose}/v1`) },
|
|
302
|
-
ikm,
|
|
303
|
-
256
|
|
304
|
-
);
|
|
305
|
-
return new Uint8Array(bits);
|
|
306
|
-
}
|
|
307
|
-
function deriveKeys(root, previous) {
|
|
308
|
-
if (root.length < MIN_ROOT_KEY) {
|
|
309
|
-
throw new CauthyConfigError(`rootKey (PACT_ROOT_KEY) must be at least ${MIN_ROOT_KEY} characters: 32 random bytes, e.g. openssl rand -base64 32`);
|
|
310
|
-
}
|
|
311
|
-
if (previous !== void 0 && previous !== "" && previous.length < MIN_ROOT_KEY) {
|
|
312
|
-
throw new CauthyConfigError(`the previous root key (PACT_ROOT_KEY_PREVIOUS) must be at least ${MIN_ROOT_KEY} characters`);
|
|
313
|
-
}
|
|
314
|
-
const roots = previous && previous !== root ? [root, previous] : [root];
|
|
315
|
-
const ring = (purpose) => Promise.all(roots.map((r) => derive(r, purpose)));
|
|
316
|
-
return { seal: ring("seal"), cookie: ring("cookie"), flow: ring("flow") };
|
|
64
|
+
return null;
|
|
317
65
|
}
|
|
318
66
|
|
|
319
67
|
// src/deadline.ts
|
|
@@ -349,6 +97,13 @@ function anySignal(signals) {
|
|
|
349
97
|
return controller.signal;
|
|
350
98
|
}
|
|
351
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
|
+
|
|
352
107
|
// src/permissions.ts
|
|
353
108
|
function matches(entry, resource, action) {
|
|
354
109
|
const i = entry.lastIndexOf(":");
|
|
@@ -418,19 +173,19 @@ var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
|
418
173
|
function policySource(system, read) {
|
|
419
174
|
const roles = Array.isArray(read) ? read : read.roles;
|
|
420
175
|
const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
|
|
421
|
-
const
|
|
422
|
-
const key = (k) => IDENT.test(k) ? k :
|
|
176
|
+
const text = (v) => JSON.stringify(v);
|
|
177
|
+
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
423
178
|
const one = (r, pad) => {
|
|
424
|
-
const parts = [`role: ${
|
|
425
|
-
if (r.name) parts.push(`name: ${
|
|
426
|
-
if (r.description) parts.push(`description: ${
|
|
427
|
-
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(", ")}]`);
|
|
428
183
|
if (r.permissions?.length) {
|
|
429
184
|
const list = r.permissions.map((p) => `${pad} { ${[
|
|
430
|
-
...p.kind && p.kind !== "*" ? [`${key("kind")}: ${
|
|
431
|
-
`resource: ${
|
|
432
|
-
`action: ${
|
|
433
|
-
...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)}`] : []
|
|
434
189
|
].join(", ")} },`);
|
|
435
190
|
parts.push(`permissions: [
|
|
436
191
|
${list.join("\n")}
|
|
@@ -442,14 +197,14 @@ ${pad} `)},
|
|
|
442
197
|
${pad} },`;
|
|
443
198
|
};
|
|
444
199
|
const resource = (c) => {
|
|
445
|
-
const parts = [`resource: ${
|
|
446
|
-
if (c.description) parts.push(`description: ${
|
|
447
|
-
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(", ")}]`);
|
|
448
203
|
const attrs = Object.entries(c.attributes ?? {});
|
|
449
|
-
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(", ")} }`);
|
|
450
205
|
return ` { ${parts.join(", ")} },`;
|
|
451
206
|
};
|
|
452
|
-
const head = `// The catalog of roles of ${
|
|
207
|
+
const head = `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
453
208
|
// Written by \`pullPolicy\`: run it again after the catalog
|
|
454
209
|
// changes, and do not edit this file by hand.
|
|
455
210
|
`;
|
|
@@ -559,7 +314,7 @@ function organizationIdOf(o) {
|
|
|
559
314
|
}
|
|
560
315
|
|
|
561
316
|
// src/system.ts
|
|
562
|
-
import { exportJWK
|
|
317
|
+
import { exportJWK, importJWK, importPKCS8, SignJWT } from "jose";
|
|
563
318
|
|
|
564
319
|
// src/theme.ts
|
|
565
320
|
var MAX_RADIUS = 40;
|
|
@@ -576,7 +331,7 @@ function withoutComments(css) {
|
|
|
576
331
|
}
|
|
577
332
|
}
|
|
578
333
|
function declarations(css) {
|
|
579
|
-
const
|
|
334
|
+
const text = withoutComments(css);
|
|
580
335
|
const out2 = [];
|
|
581
336
|
const within = [];
|
|
582
337
|
let chunk = "";
|
|
@@ -587,7 +342,7 @@ function declarations(css) {
|
|
|
587
342
|
if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
|
|
588
343
|
chunk = "";
|
|
589
344
|
};
|
|
590
|
-
for (const ch of
|
|
345
|
+
for (const ch of text) {
|
|
591
346
|
if (quote) {
|
|
592
347
|
chunk += ch;
|
|
593
348
|
if (ch === quote) quote = "";
|
|
@@ -923,6 +678,22 @@ async function pushTheme(o) {
|
|
|
923
678
|
}
|
|
924
679
|
}
|
|
925
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
|
+
|
|
926
697
|
// src/system.ts
|
|
927
698
|
var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
928
699
|
var SKEW_MS = 3e4;
|
|
@@ -962,7 +733,7 @@ function asJwk(k) {
|
|
|
962
733
|
}
|
|
963
734
|
async function loadKey(k) {
|
|
964
735
|
const jwk = asJwk(k);
|
|
965
|
-
return jwk ?
|
|
736
|
+
return jwk ? importJWK(jwk, "RS256") : importPKCS8(k.privateKey, "RS256", { extractable: true });
|
|
966
737
|
}
|
|
967
738
|
function createSystem(given = {}) {
|
|
968
739
|
const issuer = withoutTrailingSlashes(given.issuer ?? env("CAUTHY_ISSUER") ?? "");
|
|
@@ -989,10 +760,6 @@ function createSystem(given = {}) {
|
|
|
989
760
|
if (given.signer) return;
|
|
990
761
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
|
|
991
762
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
992
|
-
if (given.keys) {
|
|
993
|
-
if (!given.keysClientId) throw new CauthyConfigError("the contract gives the system no identity of its own: its link needs a service block");
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
763
|
if (key ? key.keyId === "" : clientSecret === "") {
|
|
997
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)");
|
|
998
765
|
}
|
|
@@ -1010,14 +777,14 @@ function createSystem(given = {}) {
|
|
|
1010
777
|
let asking;
|
|
1011
778
|
async function ask() {
|
|
1012
779
|
configured();
|
|
1013
|
-
const self = given.signer ? given.signer.clientId :
|
|
780
|
+
const self = given.signer ? given.signer.clientId : key?.clientId || clientId;
|
|
1014
781
|
const form = new URLSearchParams({ grant_type: "client_credentials", client_id: self });
|
|
1015
782
|
if (given.signer) {
|
|
1016
783
|
const s = await given.signer.signer;
|
|
1017
784
|
form.set("client_assertion_type", ASSERTION_TYPE);
|
|
1018
785
|
form.set("client_assertion", await s.assertion(self, tokenEndpoint));
|
|
1019
|
-
} else if (
|
|
1020
|
-
const signer =
|
|
786
|
+
} else if (key) {
|
|
787
|
+
const signer = { kid: key.keyId, privateKey: await signingKey() };
|
|
1021
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);
|
|
1022
789
|
form.set("client_assertion_type", ASSERTION_TYPE);
|
|
1023
790
|
form.set("client_assertion", assertion);
|
|
@@ -1126,9 +893,8 @@ function createSystem(given = {}) {
|
|
|
1126
893
|
return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
|
|
1127
894
|
},
|
|
1128
895
|
async publicJwks() {
|
|
1129
|
-
if (given.keys) return given.keys.published();
|
|
1130
896
|
if (!key) return { keys: [] };
|
|
1131
|
-
const { kty, n, e } = asJwk(key) ?? await
|
|
897
|
+
const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
|
|
1132
898
|
return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
|
|
1133
899
|
}
|
|
1134
900
|
};
|
|
@@ -1462,8 +1228,8 @@ var fail = (reason, message) => {
|
|
|
1462
1228
|
throw new CauthyKeyError(reason, message);
|
|
1463
1229
|
};
|
|
1464
1230
|
var NOT_A_KEY = "not a Cauthy key (copy it again from the dashboard)";
|
|
1465
|
-
function parseKey(
|
|
1466
|
-
const s =
|
|
1231
|
+
function parseKey(text) {
|
|
1232
|
+
const s = text.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, "");
|
|
1467
1233
|
if (s === "" || s.length > MAX_LEN || !printableAscii(s)) fail("key_format", NOT_A_KEY);
|
|
1468
1234
|
const head = s.split(".", 1)[0];
|
|
1469
1235
|
if (head !== KEY_PREFIX) {
|
|
@@ -1646,10 +1412,10 @@ async function finish(res, route, cacheable = false) {
|
|
|
1646
1412
|
if (!cacheable && !(headers.get("cache-control") ?? "").includes("no-store")) headers.set("cache-control", "no-store");
|
|
1647
1413
|
let body = res.body;
|
|
1648
1414
|
if (route !== null && SIGN_IN_ROUTES.has(route) && res.ok && (headers.get("content-type") ?? "").startsWith("application/json")) {
|
|
1649
|
-
const
|
|
1650
|
-
body =
|
|
1415
|
+
const text = await res.text();
|
|
1416
|
+
body = text;
|
|
1651
1417
|
try {
|
|
1652
|
-
const parsed = JSON.parse(
|
|
1418
|
+
const parsed = JSON.parse(text);
|
|
1653
1419
|
const next = nextOf(route, parsed?.status);
|
|
1654
1420
|
if (parsed !== null && next !== void 0) body = JSON.stringify({ ...parsed, next });
|
|
1655
1421
|
} catch {
|
|
@@ -1665,24 +1431,24 @@ import { compactVerify, createLocalJWKSet } from "jose";
|
|
|
1665
1431
|
// src/json.ts
|
|
1666
1432
|
var SPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
|
|
1667
1433
|
var LIMIT = 2n ** 53n;
|
|
1668
|
-
function parseStrict(
|
|
1434
|
+
function parseStrict(text, integerMembers = []) {
|
|
1669
1435
|
let i = 0;
|
|
1670
1436
|
let depth = 0;
|
|
1671
1437
|
const fail2 = (what) => {
|
|
1672
1438
|
throw new SyntaxError(`${what} at ${i}`);
|
|
1673
1439
|
};
|
|
1674
1440
|
const skip = () => {
|
|
1675
|
-
while (i <
|
|
1441
|
+
while (i < text.length && SPACE.has(text[i])) i++;
|
|
1676
1442
|
};
|
|
1677
1443
|
const value = (integer = false) => {
|
|
1678
1444
|
skip();
|
|
1679
|
-
const c =
|
|
1445
|
+
const c = text[i];
|
|
1680
1446
|
if (c === "{") return object();
|
|
1681
1447
|
if (c === "[") return array();
|
|
1682
1448
|
if (c === '"') return string();
|
|
1683
1449
|
if (c === "-" || c >= "0" && c <= "9") return number(integer);
|
|
1684
1450
|
for (const [word, v] of [["true", true], ["false", false], ["null", null]]) {
|
|
1685
|
-
if (
|
|
1451
|
+
if (text.startsWith(word, i)) {
|
|
1686
1452
|
i += word.length;
|
|
1687
1453
|
return v;
|
|
1688
1454
|
}
|
|
@@ -1692,13 +1458,13 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1692
1458
|
const string = () => {
|
|
1693
1459
|
const start = i;
|
|
1694
1460
|
i++;
|
|
1695
|
-
while (i <
|
|
1696
|
-
if (
|
|
1461
|
+
while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1;
|
|
1462
|
+
if (text[i] !== '"') fail2("unterminated string");
|
|
1697
1463
|
i++;
|
|
1698
|
-
return JSON.parse(
|
|
1464
|
+
return JSON.parse(text.slice(start, i));
|
|
1699
1465
|
};
|
|
1700
1466
|
const number = (integer) => {
|
|
1701
|
-
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(
|
|
1467
|
+
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(i));
|
|
1702
1468
|
if (!m) return fail2("bad number");
|
|
1703
1469
|
i += m[0].length;
|
|
1704
1470
|
if (integer && (m[2] !== void 0 || m[3] !== void 0)) return Number.NaN;
|
|
@@ -1713,7 +1479,7 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1713
1479
|
depth++;
|
|
1714
1480
|
const out2 = [];
|
|
1715
1481
|
skip();
|
|
1716
|
-
if (
|
|
1482
|
+
if (text[i] === "]") {
|
|
1717
1483
|
i++;
|
|
1718
1484
|
depth--;
|
|
1719
1485
|
return out2;
|
|
@@ -1721,8 +1487,8 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1721
1487
|
for (; ; ) {
|
|
1722
1488
|
out2.push(value());
|
|
1723
1489
|
skip();
|
|
1724
|
-
if (
|
|
1725
|
-
else if (
|
|
1490
|
+
if (text[i] === ",") i++;
|
|
1491
|
+
else if (text[i] === "]") {
|
|
1726
1492
|
i++;
|
|
1727
1493
|
depth--;
|
|
1728
1494
|
return out2;
|
|
@@ -1735,24 +1501,24 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1735
1501
|
const out2 = {};
|
|
1736
1502
|
const seen = /* @__PURE__ */ new Set();
|
|
1737
1503
|
skip();
|
|
1738
|
-
if (
|
|
1504
|
+
if (text[i] === "}") {
|
|
1739
1505
|
i++;
|
|
1740
1506
|
depth--;
|
|
1741
1507
|
return out2;
|
|
1742
1508
|
}
|
|
1743
1509
|
for (; ; ) {
|
|
1744
1510
|
skip();
|
|
1745
|
-
if (
|
|
1511
|
+
if (text[i] !== '"') fail2("expected a name");
|
|
1746
1512
|
const name = string();
|
|
1747
1513
|
if (seen.has(name)) fail2(`repeated name ${JSON.stringify(name)}`);
|
|
1748
1514
|
seen.add(name);
|
|
1749
1515
|
skip();
|
|
1750
|
-
if (
|
|
1516
|
+
if (text[i] !== ":") fail2("expected :");
|
|
1751
1517
|
i++;
|
|
1752
1518
|
Object.defineProperty(out2, name, { value: value(depth === 1 && integerMembers.includes(name)), enumerable: true, writable: true, configurable: true });
|
|
1753
1519
|
skip();
|
|
1754
|
-
if (
|
|
1755
|
-
else if (
|
|
1520
|
+
if (text[i] === ",") i++;
|
|
1521
|
+
else if (text[i] === "}") {
|
|
1756
1522
|
i++;
|
|
1757
1523
|
depth--;
|
|
1758
1524
|
return out2;
|
|
@@ -1761,7 +1527,7 @@ function parseStrict(text2, integerMembers = []) {
|
|
|
1761
1527
|
};
|
|
1762
1528
|
const result = value();
|
|
1763
1529
|
skip();
|
|
1764
|
-
if (i <
|
|
1530
|
+
if (i < text.length) fail2("trailing characters");
|
|
1765
1531
|
return result;
|
|
1766
1532
|
}
|
|
1767
1533
|
|
|
@@ -1782,6 +1548,15 @@ function deprecated(what, instead) {
|
|
|
1782
1548
|
var enc2 = new TextEncoder();
|
|
1783
1549
|
var PKCS8_ED25519 = Uint8Array.from([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]);
|
|
1784
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)));
|
|
1785
1560
|
async function seedOf(secret) {
|
|
1786
1561
|
const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
|
|
1787
1562
|
return new Uint8Array(await crypto.subtle.deriveBits(
|
|
@@ -1819,12 +1594,11 @@ async function signerOf(secret) {
|
|
|
1819
1594
|
}
|
|
1820
1595
|
function deriveFromKey(key, previous) {
|
|
1821
1596
|
const other = previous && previous.secret !== key.secret ? previous : void 0;
|
|
1822
|
-
const
|
|
1597
|
+
const secrets = other ? [key.secret, other.secret] : [key.secret];
|
|
1823
1598
|
return {
|
|
1824
|
-
cookie:
|
|
1825
|
-
flow:
|
|
1826
|
-
signer: signerOf(key.secret)
|
|
1827
|
-
...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)
|
|
1828
1602
|
};
|
|
1829
1603
|
}
|
|
1830
1604
|
|
|
@@ -1943,8 +1717,8 @@ var CauthyTokenError = class extends Error {
|
|
|
1943
1717
|
detail;
|
|
1944
1718
|
};
|
|
1945
1719
|
var MAX_TOKEN_BYTES = 8192;
|
|
1946
|
-
var
|
|
1947
|
-
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";
|
|
1948
1722
|
var SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
1949
1723
|
var MAX_KEYS_BYTES = 64 * 1024;
|
|
1950
1724
|
var KEYS_TIMEOUT_MS = 5e3;
|
|
@@ -2024,7 +1798,7 @@ async function readCapped(res, max) {
|
|
|
2024
1798
|
}
|
|
2025
1799
|
return out2;
|
|
2026
1800
|
}
|
|
2027
|
-
var
|
|
1801
|
+
var published = /* @__PURE__ */ new Map();
|
|
2028
1802
|
var withFetch = /* @__PURE__ */ new WeakMap();
|
|
2029
1803
|
function remoteKeysFor(issuer, fetchKeys) {
|
|
2030
1804
|
let byIssuer = withFetch.get(fetchKeys);
|
|
@@ -2034,10 +1808,10 @@ function remoteKeysFor(issuer, fetchKeys) {
|
|
|
2034
1808
|
return set;
|
|
2035
1809
|
}
|
|
2036
1810
|
function keysOf2(issuer) {
|
|
2037
|
-
let set =
|
|
1811
|
+
let set = published.get(issuer);
|
|
2038
1812
|
if (set === void 0) {
|
|
2039
1813
|
set = remoteKeys(issuer);
|
|
2040
|
-
|
|
1814
|
+
published.set(issuer, set);
|
|
2041
1815
|
}
|
|
2042
1816
|
return set;
|
|
2043
1817
|
}
|
|
@@ -2079,7 +1853,7 @@ function createTokenVerifier(options = {}) {
|
|
|
2079
1853
|
if (audience === "") throw new CauthyConfigError("audience (CAUTHY_AUDIENCE) is not set: without it a token issued for another application would be accepted");
|
|
2080
1854
|
const tenant = options.tenantId ?? env2("CAUTHY_TENANT_ID") ?? "";
|
|
2081
1855
|
if (tenant === "") throw new CauthyConfigError("tenantId (CAUTHY_TENANT_ID) is not set: without it a token of another workspace would be accepted");
|
|
2082
|
-
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");
|
|
2083
1857
|
const leeway = options.leeway ?? 30;
|
|
2084
1858
|
if (!Number.isFinite(leeway) || leeway < 0 || leeway > 60) throw new CauthyConfigError("leeway must be 0 to 60 seconds");
|
|
2085
1859
|
return async (from) => {
|
|
@@ -2128,8 +1902,8 @@ function createTokenVerifier(options = {}) {
|
|
|
2128
1902
|
throw new CauthyTokenError("invalid_claim", name);
|
|
2129
1903
|
};
|
|
2130
1904
|
if (typeof claims.iss !== "string") bad("iss");
|
|
2131
|
-
if (typeof claims.sub !== "string" || !
|
|
2132
|
-
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");
|
|
2133
1907
|
const aud = claims.aud;
|
|
2134
1908
|
if (!(typeof aud === "string" || Array.isArray(aud) && aud.length > 0 && aud.every((a) => typeof a === "string"))) bad("aud");
|
|
2135
1909
|
for (const name of ["exp", "iat", "nbf"]) if (claims[name] !== void 0 && !Number.isInteger(claims[name])) bad(name);
|
|
@@ -2167,13 +1941,13 @@ async function verifyToken(from, options = {}) {
|
|
|
2167
1941
|
return createTokenVerifier(options)(from);
|
|
2168
1942
|
}
|
|
2169
1943
|
async function createVerifierFromEnv(options = {}) {
|
|
2170
|
-
const
|
|
2171
|
-
if (
|
|
1944
|
+
const text = options.key ?? env2("CAUTHY_KEY");
|
|
1945
|
+
if (text === void 0 || text === "") return createTokenVerifier(options);
|
|
2172
1946
|
refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
|
|
2173
1947
|
for (const name of ["issuer", "audience", "tenantId"]) {
|
|
2174
1948
|
if (options[name] !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
|
|
2175
1949
|
}
|
|
2176
|
-
const key = parseKey(
|
|
1950
|
+
const key = parseKey(text);
|
|
2177
1951
|
const system = createSystem({ issuer: key.issuer, clientId: key.clientId, signer: { clientId: key.clientId, signer: deriveFromKey(key).signer }, fetch: options.fetch });
|
|
2178
1952
|
const id = await createIdentity({ issuer: key.issuer, token: () => system.token(), fetch: deadline(options.fetch, KEYS_TIMEOUT_MS) }).ready();
|
|
2179
1953
|
const { key: _, ...rest } = options;
|
|
@@ -2270,9 +2044,18 @@ function redirect(location, cookies = []) {
|
|
|
2270
2044
|
function conditionalOf(c) {
|
|
2271
2045
|
return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
|
|
2272
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
|
+
}
|
|
2273
2056
|
function byKey(given) {
|
|
2274
|
-
const
|
|
2275
|
-
if (
|
|
2057
|
+
const text = given.key ?? env3("CAUTHY_KEY");
|
|
2058
|
+
if (text === void 0 || text === "") return void 0;
|
|
2276
2059
|
refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
|
|
2277
2060
|
for (const [name, value] of [
|
|
2278
2061
|
["issuer", given.issuer],
|
|
@@ -2283,12 +2066,11 @@ function byKey(given) {
|
|
|
2283
2066
|
["tenant", given.tenant],
|
|
2284
2067
|
["audience", given.audience],
|
|
2285
2068
|
["tenantId", given.tenantId],
|
|
2286
|
-
["systemKey", given.systemKey]
|
|
2287
|
-
["rootKey", given.rootKey]
|
|
2069
|
+
["systemKey", given.systemKey]
|
|
2288
2070
|
]) {
|
|
2289
2071
|
if (value !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
|
|
2290
2072
|
}
|
|
2291
|
-
const key = parseKey(
|
|
2073
|
+
const key = parseKey(text);
|
|
2292
2074
|
const prevText = given.keyPrevious ?? env3("CAUTHY_KEY_PREVIOUS");
|
|
2293
2075
|
let previous;
|
|
2294
2076
|
if (prevText) {
|
|
@@ -2303,13 +2085,8 @@ function byKey(given) {
|
|
|
2303
2085
|
throw new CauthyKeyError("key_previous_mismatch", "CAUTHY_KEY_PREVIOUS names another Cauthy or client than CAUTHY_KEY");
|
|
2304
2086
|
}
|
|
2305
2087
|
const legacySecret = env3("CAUTHY_SECRET") || void 0;
|
|
2306
|
-
const legacyRoot = env3("PACT_ROOT_KEY") || void 0;
|
|
2307
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.");
|
|
2308
|
-
|
|
2309
|
-
if (given.keys && !(legacyRoot && given.contract)) {
|
|
2310
|
-
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)");
|
|
2311
|
-
}
|
|
2312
|
-
return { key, previous, legacySecret, legacyRoot };
|
|
2089
|
+
return { key, previous, legacySecret };
|
|
2313
2090
|
}
|
|
2314
2091
|
var UnknownOriginError = class extends Error {
|
|
2315
2092
|
constructor(origin, registered) {
|
|
@@ -2320,72 +2097,41 @@ var UnknownOriginError = class extends Error {
|
|
|
2320
2097
|
origin;
|
|
2321
2098
|
registered;
|
|
2322
2099
|
};
|
|
2323
|
-
function contractOnKey(given, keyed) {
|
|
2324
|
-
const contract = readContract(given.contract);
|
|
2325
|
-
if (contract.issuer !== keyed.key.issuer) {
|
|
2326
|
-
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`);
|
|
2327
|
-
}
|
|
2328
|
-
return contract;
|
|
2329
|
-
}
|
|
2330
|
-
function byContract(given) {
|
|
2331
|
-
failClosed(typeof process !== "undefined" ? process.env : void 0);
|
|
2332
|
-
for (const [name, value] of [
|
|
2333
|
-
["clientId", given.clientId],
|
|
2334
|
-
["clientSecret", given.clientSecret],
|
|
2335
|
-
["secret", given.secret],
|
|
2336
|
-
["system", given.system],
|
|
2337
|
-
["tenant", given.tenant],
|
|
2338
|
-
["systemKey", given.systemKey]
|
|
2339
|
-
]) {
|
|
2340
|
-
if (value !== void 0) throw new CauthyConfigError(`with a contract, ${name} comes from the contract: do not pass it`);
|
|
2341
|
-
}
|
|
2342
|
-
const contract = readContract(given.contract);
|
|
2343
|
-
const root = given.rootKey ?? env3("PACT_ROOT_KEY") ?? "";
|
|
2344
|
-
if (root === "") throw new CauthyConfigError("rootKey (PACT_ROOT_KEY) is required with a contract: 32 random bytes, e.g. openssl rand -base64 32");
|
|
2345
|
-
if (contract.serviceClientId && !given.keys) {
|
|
2346
|
-
throw new CauthyConfigError(`the link ${contract.link} gives ${contract.system} its own identity: pass keys, where its key is kept (@cauthyhq/node/keys)`);
|
|
2347
|
-
}
|
|
2348
|
-
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.");
|
|
2349
|
-
return { contract, root, previous: given.rootKeyPrevious ?? env3("PACT_ROOT_KEY_PREVIOUS") };
|
|
2350
|
-
}
|
|
2351
2100
|
var toldOfHeader = false;
|
|
2101
|
+
var toldOfUnopened = false;
|
|
2102
|
+
function toldUnopened() {
|
|
2103
|
+
if (toldOfUnopened) return;
|
|
2104
|
+
toldOfUnopened = true;
|
|
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.");
|
|
2106
|
+
}
|
|
2352
2107
|
function createCauthy(given = {}) {
|
|
2108
|
+
refuseContracts(given);
|
|
2353
2109
|
const keyed = byKey(given);
|
|
2354
|
-
const
|
|
2355
|
-
const
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2358
|
-
const
|
|
2359
|
-
const
|
|
2360
|
-
const askSelf = keyed !== void 0 && contract === void 0;
|
|
2361
|
-
const system = askSelf ? "" : contract?.system ?? given.system ?? env3("CAUTHY_SYSTEM") ?? "";
|
|
2362
|
-
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") ?? "";
|
|
2363
2116
|
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
|
|
2364
2117
|
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
2365
|
-
if (!
|
|
2118
|
+
if (!keyed && secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
|
|
2366
2119
|
const fromKey = keyed ? deriveFromKey(keyed.key, keyed.previous) : void 0;
|
|
2367
2120
|
let signingKid;
|
|
2368
2121
|
fromKey?.signer.then((s) => {
|
|
2369
2122
|
signingKid = s.kid;
|
|
2370
2123
|
}, () => void 0);
|
|
2371
|
-
const derived = pacted ? deriveKeys(pacted.root, pacted.previous) : void 0;
|
|
2372
|
-
const single = derived || fromKey ? void 0 : keyOf(secret).then((k) => [k]);
|
|
2373
|
-
const legacyRings = keyed?.legacyRoot ? deriveKeys(keyed.legacyRoot) : void 0;
|
|
2374
2124
|
const legacySingle = keyed?.legacySecret ? keyOf(keyed.legacySecret) : void 0;
|
|
2375
|
-
const withLegacy = async (own
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
const cookieKeys = fromKey ? withLegacy(fromKey.cookie, "cookie") : derived?.cookie ?? single;
|
|
2381
|
-
const flowKeys = fromKey ? withLegacy(fromKey.flow, "flow") : derived?.flow ?? single;
|
|
2382
|
-
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) {
|
|
2383
2130
|
deprecated(
|
|
2384
2131
|
"Configuring with CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_CLIENT_SECRET, CAUTHY_SECRET and the other CAUTHY_* variables (or their options)",
|
|
2385
2132
|
"Connect the application in Cauthy's dashboard and use the one variable CAUTHY_KEY."
|
|
2386
2133
|
);
|
|
2387
2134
|
}
|
|
2388
|
-
const ownKeys = derived && given.keys ? pactKeys(given.keys, derived.seal) : legacyRings && contract && given.keys ? pactKeys(given.keys, legacyRings.seal) : void 0;
|
|
2389
2135
|
const baseURL = given.baseURL ?? (keyed ? void 0 : env3("CAUTHY_BASE_URL"));
|
|
2390
2136
|
if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
|
|
2391
2137
|
throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
|
|
@@ -2422,21 +2168,10 @@ function createCauthy(given = {}) {
|
|
|
2422
2168
|
acceptInviteUrl,
|
|
2423
2169
|
fetch: given.fetch,
|
|
2424
2170
|
timeoutMs: systemTimeout,
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
...fromKey ? {
|
|
2428
|
-
signer: { clientId: contract ? contract.serviceClientId ?? keyed.key.clientId : clientId, signer: fromKey.signer },
|
|
2429
|
-
currentSystem: () => identity?.current()?.system ?? ""
|
|
2430
|
-
} : {}
|
|
2171
|
+
// On CAUTHY_KEY the key proves the key's own client.
|
|
2172
|
+
...fromKey ? { signer: { clientId, signer: fromKey.signer }, currentSystem: () => identity?.current()?.system ?? "" } : {}
|
|
2431
2173
|
});
|
|
2432
|
-
const
|
|
2433
|
-
issuer,
|
|
2434
|
-
token: () => ownSystem.token(),
|
|
2435
|
-
fetch: deadline(given.fetch, requestTimeout),
|
|
2436
|
-
kid: () => signingKid,
|
|
2437
|
-
log: (m) => console.error(`@cauthyhq/node: ${m}`)
|
|
2438
|
-
});
|
|
2439
|
-
const identity = askSelf ? createIdentity({
|
|
2174
|
+
const identity = keyed ? createIdentity({
|
|
2440
2175
|
issuer,
|
|
2441
2176
|
token: () => ownSystem.token(),
|
|
2442
2177
|
fetch: deadline(given.fetch, requestTimeout),
|
|
@@ -2446,6 +2181,8 @@ function createCauthy(given = {}) {
|
|
|
2446
2181
|
identity?.ready().catch(() => {
|
|
2447
2182
|
});
|
|
2448
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;
|
|
2449
2186
|
const systemNow = () => identity?.current()?.system ?? system;
|
|
2450
2187
|
const requestOrigin = (request) => {
|
|
2451
2188
|
const url = new URL(request.url);
|
|
@@ -2470,7 +2207,7 @@ function createCauthy(given = {}) {
|
|
|
2470
2207
|
};
|
|
2471
2208
|
const attrs = (request, maxAge) => ({ secure: safeOrigin(request).startsWith("https://"), maxAge });
|
|
2472
2209
|
async function clientAuth(form) {
|
|
2473
|
-
if (fromKey &&
|
|
2210
|
+
if (fromKey && form.get("client_id") === clientId) {
|
|
2474
2211
|
const s = await fromKey.signer;
|
|
2475
2212
|
form.set("client_assertion_type", ASSERTION_TYPE2);
|
|
2476
2213
|
form.set("client_assertion", await s.assertion(clientId, `${issuer}/v1/oauth/token`));
|
|
@@ -2480,8 +2217,8 @@ function createCauthy(given = {}) {
|
|
|
2480
2217
|
return form;
|
|
2481
2218
|
}
|
|
2482
2219
|
async function token(form, proof = {}) {
|
|
2483
|
-
const body2 = await clientAuth(new URLSearchParams({ client_id: clientId, ...form }));
|
|
2484
2220
|
try {
|
|
2221
|
+
const body2 = await clientAuth(new URLSearchParams({ client_id: await signInClient(), ...form }));
|
|
2485
2222
|
const res = await doFetch(`${issuer}/v1/oauth/token`, {
|
|
2486
2223
|
method: "POST",
|
|
2487
2224
|
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json", ...proof },
|
|
@@ -2519,18 +2256,16 @@ function createCauthy(given = {}) {
|
|
|
2519
2256
|
}
|
|
2520
2257
|
const kindOf = (body2, was) => (typeof body2?.persistent === "boolean" ? body2.persistent : was?.p === 1) ? { p: 1 } : {};
|
|
2521
2258
|
async function revoke(refreshToken) {
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
+
}
|
|
2524
2264
|
}
|
|
2525
|
-
const unverified = (u) => contract?.requireVerifiedEmail === true && !u.emailVerified;
|
|
2526
2265
|
async function stored(body2, was) {
|
|
2527
2266
|
if (typeof body2?.access_token !== "string" || typeof body2?.refresh_token !== "string") return null;
|
|
2528
2267
|
const who = await userinfo(body2.access_token);
|
|
2529
2268
|
if (who === null || who.u.id === "") return null;
|
|
2530
|
-
if (unverified(who.u)) {
|
|
2531
|
-
await revoke(body2.refresh_token);
|
|
2532
|
-
throw new CauthyApiError(403, "email_not_confirmed", "Confirm your email address first.");
|
|
2533
|
-
}
|
|
2534
2269
|
const lifetime = typeof body2.expires_in === "number" && body2.expires_in > 0 ? body2.expires_in : 300;
|
|
2535
2270
|
return { at: body2.access_token, rt: body2.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who, ...kindOf(body2, was) };
|
|
2536
2271
|
}
|
|
@@ -2540,10 +2275,11 @@ function createCauthy(given = {}) {
|
|
|
2540
2275
|
async function read(request) {
|
|
2541
2276
|
const had = readCookies(request.headers.get("cookie"));
|
|
2542
2277
|
const raw = readParts(had, cookieName);
|
|
2543
|
-
if (raw === null) return { had, value: null, present: false };
|
|
2544
|
-
const
|
|
2278
|
+
if (raw === null) return { had, value: null, present: false, stale: false };
|
|
2279
|
+
const opened = await unsealIndexed(raw, await cookieKeys);
|
|
2280
|
+
const value = opened?.value ?? null;
|
|
2545
2281
|
const sound = value !== null && typeof value.at === "string" && typeof value.rt === "string" && typeof value.exp === "number";
|
|
2546
|
-
return { had, value: sound ? value : null, present: true };
|
|
2282
|
+
return { had, value: sound ? value : null, present: true, stale: sound && opened.index > 0 };
|
|
2547
2283
|
}
|
|
2548
2284
|
async function write(request, s, had) {
|
|
2549
2285
|
return writeParts(cookieName, await seal(s, (await cookieKeys)[0]), attrs(request, s.p === 1 ? PERSISTENT_SECONDS : void 0), had);
|
|
@@ -2557,13 +2293,7 @@ function createCauthy(given = {}) {
|
|
|
2557
2293
|
running = (async () => {
|
|
2558
2294
|
const res = await token({ grant_type: "refresh_token", refresh_token: rt });
|
|
2559
2295
|
if (!res.ok) return res.ended ? "ended" : "unreachable";
|
|
2560
|
-
|
|
2561
|
-
try {
|
|
2562
|
-
next = await stored(res.body, was);
|
|
2563
|
-
} catch (err) {
|
|
2564
|
-
if (err instanceof CauthyApiError && err.code === "email_not_confirmed") return "ended";
|
|
2565
|
-
throw err;
|
|
2566
|
-
}
|
|
2296
|
+
const next = await stored(res.body, was);
|
|
2567
2297
|
if (next !== null) return next;
|
|
2568
2298
|
if (typeof res.body?.access_token !== "string" || typeof res.body?.refresh_token !== "string") return "unreachable";
|
|
2569
2299
|
const lifetime = typeof res.body.expires_in === "number" && res.body.expires_in > 0 ? res.body.expires_in : 300;
|
|
@@ -2580,12 +2310,12 @@ function createCauthy(given = {}) {
|
|
|
2580
2310
|
return running;
|
|
2581
2311
|
}
|
|
2582
2312
|
async function resolve2(request) {
|
|
2583
|
-
const { had, value, present } = await read(request);
|
|
2313
|
+
const { had, value, present, stale } = await read(request);
|
|
2584
2314
|
if (value === null) {
|
|
2315
|
+
if (present) toldUnopened();
|
|
2585
2316
|
return { session: null, status: "unauthenticated", cookies: present ? clearParts(cookieName, attrs(request, 0), had) : [] };
|
|
2586
2317
|
}
|
|
2587
|
-
if (
|
|
2588
|
-
if (fresh(value)) return { session: toSession(value), status: "authenticated", cookies: [] };
|
|
2318
|
+
if (fresh(value)) return { session: toSession(value), status: "authenticated", cookies: stale ? await write(request, value, had) : [] };
|
|
2589
2319
|
const renewed = await renew(value);
|
|
2590
2320
|
if (renewed === "ended") {
|
|
2591
2321
|
return { session: null, status: "unauthenticated", cookies: clearParts(cookieName, attrs(request, 0), had) };
|
|
@@ -2596,7 +2326,7 @@ function createCauthy(given = {}) {
|
|
|
2596
2326
|
async function getSession(request, responseHeaders) {
|
|
2597
2327
|
if (responseHeaders === void 0) {
|
|
2598
2328
|
const { value } = await read(request);
|
|
2599
|
-
return value !== null && fresh(value)
|
|
2329
|
+
return value !== null && fresh(value) ? toSession(value) : null;
|
|
2600
2330
|
}
|
|
2601
2331
|
const r = await resolve2(request);
|
|
2602
2332
|
for (const c of r.cookies) responseHeaders.append("set-cookie", c);
|
|
@@ -2604,7 +2334,7 @@ function createCauthy(given = {}) {
|
|
|
2604
2334
|
}
|
|
2605
2335
|
async function status(request) {
|
|
2606
2336
|
const { value } = await read(request);
|
|
2607
|
-
if (value === null
|
|
2337
|
+
if (value === null) return "unauthenticated";
|
|
2608
2338
|
return fresh(value) ? "authenticated" : "expired";
|
|
2609
2339
|
}
|
|
2610
2340
|
const orgCache = /* @__PURE__ */ new Map();
|
|
@@ -2691,7 +2421,7 @@ function createCauthy(given = {}) {
|
|
|
2691
2421
|
const flow = { state: random(), verifier: random(48), returnTo: localPath(url.searchParams.get("return_to"), afterSignIn) };
|
|
2692
2422
|
const challenge = b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(flow.verifier))));
|
|
2693
2423
|
const q = new URLSearchParams({
|
|
2694
|
-
client_id:
|
|
2424
|
+
client_id: await signInClient(),
|
|
2695
2425
|
redirect_uri: `${originOf(request)}${basePath}/callback`,
|
|
2696
2426
|
response_type: "code",
|
|
2697
2427
|
state: flow.state,
|
|
@@ -2753,28 +2483,17 @@ function createCauthy(given = {}) {
|
|
|
2753
2483
|
);
|
|
2754
2484
|
};
|
|
2755
2485
|
if (!res.ok && res.code === "email_not_confirmed") return unconfirmed();
|
|
2756
|
-
|
|
2757
|
-
try {
|
|
2758
|
-
s = res.ok ? await stored(res.body) : null;
|
|
2759
|
-
} catch (err) {
|
|
2760
|
-
if (!(err instanceof CauthyApiError && err.code === "email_not_confirmed")) throw err;
|
|
2761
|
-
return unconfirmed();
|
|
2762
|
-
}
|
|
2486
|
+
const s = res.ok ? await stored(res.body) : null;
|
|
2763
2487
|
if (s === null) return failed(request, "exchange_failed");
|
|
2764
2488
|
return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]);
|
|
2765
2489
|
}
|
|
2766
2490
|
async function logout(request) {
|
|
2767
2491
|
const { had, value } = await read(request);
|
|
2768
2492
|
if (value !== null) {
|
|
2769
|
-
|
|
2770
|
-
await doFetch(`${issuer}/v1/oauth/revoke`, {
|
|
2771
|
-
method: "POST",
|
|
2772
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
2773
|
-
body: body2
|
|
2774
|
-
}).catch(() => void 0);
|
|
2493
|
+
await revoke(value.rt);
|
|
2775
2494
|
orgCache.delete(value.at);
|
|
2776
2495
|
}
|
|
2777
|
-
const q = new URLSearchParams({ client_id:
|
|
2496
|
+
const q = new URLSearchParams({ client_id: signInClientNow(), post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
|
|
2778
2497
|
return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
|
|
2779
2498
|
}
|
|
2780
2499
|
const STEP_SECONDS = 600;
|
|
@@ -2792,7 +2511,7 @@ function createCauthy(given = {}) {
|
|
|
2792
2511
|
createdAt: String(d?.created_at ?? ""),
|
|
2793
2512
|
lastUsedAt: d?.last_used_at ? String(d.last_used_at) : null
|
|
2794
2513
|
});
|
|
2795
|
-
const
|
|
2514
|
+
const UUID2 = /^[0-9a-f-]{36}$/i;
|
|
2796
2515
|
const seatOf = (d) => ({
|
|
2797
2516
|
id: String(d?.id ?? ""),
|
|
2798
2517
|
system: String(d?.system ?? ""),
|
|
@@ -2897,10 +2616,10 @@ function createCauthy(given = {}) {
|
|
|
2897
2616
|
...page ? { "x-cauthy-link-to": `${originOf(request)}${page}` } : { "x-cauthy-links": "hosted" },
|
|
2898
2617
|
// Whose request it is: what Cauthy mails goes in this application's
|
|
2899
2618
|
// name, under its logo.
|
|
2900
|
-
...
|
|
2619
|
+
...signInClientNow() ? { "x-cauthy-client-id": signInClientNow() } : {},
|
|
2901
2620
|
...await passedOn(request),
|
|
2902
2621
|
...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
|
|
2903
|
-
// 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.
|
|
2904
2623
|
...remember ? { "x-cauthy-remember": "1" } : {},
|
|
2905
2624
|
// The answer to the workspace's Turnstile challenge, from the
|
|
2906
2625
|
// person's browser: Cauthy checks it at the doors that ask for it.
|
|
@@ -3041,7 +2760,7 @@ function createCauthy(given = {}) {
|
|
|
3041
2760
|
}
|
|
3042
2761
|
if (route === "/look" && !post) {
|
|
3043
2762
|
if (lookCache === null || lookCache.until < Date.now()) {
|
|
3044
|
-
const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(
|
|
2763
|
+
const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(signInClientNow())}`);
|
|
3045
2764
|
lookCache = { until: Date.now() + LOOK_TTL_MS, look: res.error ? {} : res.data ?? {} };
|
|
3046
2765
|
}
|
|
3047
2766
|
return json(lookCache.look);
|
|
@@ -3066,7 +2785,7 @@ function createCauthy(given = {}) {
|
|
|
3066
2785
|
signUp: settings.data?.allow_signup !== false && settings.data?.signup?.mode !== "invite_only",
|
|
3067
2786
|
// The application's system makes an organization per sign-up: the form asks the company's name.
|
|
3068
2787
|
...settings.data?.signup?.organization?.ask === true ? { organization: { ask: true, required: settings.data.signup.organization.required === true } } : {},
|
|
3069
|
-
// 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).
|
|
3070
2789
|
...typeof settings.data?.persistent_sessions === "boolean" ? { persistentSessions: settings.data.persistent_sessions } : {},
|
|
3071
2790
|
// A sign-in link needs a page of the application to open.
|
|
3072
2791
|
magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
|
|
@@ -3230,7 +2949,7 @@ function createCauthy(given = {}) {
|
|
|
3230
2949
|
let path = "/v1/auth/me/organizations";
|
|
3231
2950
|
if (route !== "/me/organizations") {
|
|
3232
2951
|
const id = url.searchParams.get("id") ?? "";
|
|
3233
|
-
if (!
|
|
2952
|
+
if (!UUID2.test(id)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3234
2953
|
path += `/${id}/${route.endsWith("/roles") ? "roles" : "members"}`;
|
|
3235
2954
|
} else if (url.searchParams.get("system")) {
|
|
3236
2955
|
path += `?system=${encodeURIComponent(url.searchParams.get("system"))}`;
|
|
@@ -3291,8 +3010,7 @@ function createCauthy(given = {}) {
|
|
|
3291
3010
|
if (route === "/sign-out") {
|
|
3292
3011
|
const { had, value } = await read(request);
|
|
3293
3012
|
if (value !== null) {
|
|
3294
|
-
|
|
3295
|
-
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);
|
|
3296
3014
|
orgCache.delete(value.at);
|
|
3297
3015
|
}
|
|
3298
3016
|
const h = new Headers();
|
|
@@ -3519,7 +3237,7 @@ function createCauthy(given = {}) {
|
|
|
3519
3237
|
}
|
|
3520
3238
|
if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
|
|
3521
3239
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3522
|
-
if (!
|
|
3240
|
+
if (!UUID2.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3523
3241
|
const path = `/v1/auth/me/organizations/${org}`;
|
|
3524
3242
|
if (route === "/me/organizations/rename") {
|
|
3525
3243
|
const bad = checkOrganizationName(b.name);
|
|
@@ -3536,7 +3254,7 @@ function createCauthy(given = {}) {
|
|
|
3536
3254
|
}
|
|
3537
3255
|
if (route.startsWith("/me/organizations/members/")) {
|
|
3538
3256
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3539
|
-
if (!
|
|
3257
|
+
if (!UUID2.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
|
|
3540
3258
|
const base = `/v1/auth/me/organizations/${org}/members`;
|
|
3541
3259
|
if (route === "/me/organizations/members/add") {
|
|
3542
3260
|
const bad = checkEmail(b.email);
|
|
@@ -3547,7 +3265,7 @@ function createCauthy(given = {}) {
|
|
|
3547
3265
|
email: String(b.email).trim(),
|
|
3548
3266
|
role,
|
|
3549
3267
|
name: typeof b.name === "string" ? b.name.trim() : "",
|
|
3550
|
-
client_id:
|
|
3268
|
+
client_id: signInClientNow(),
|
|
3551
3269
|
only_new: true,
|
|
3552
3270
|
...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {}
|
|
3553
3271
|
}, pages.acceptInvite, mine.token);
|
|
@@ -3556,7 +3274,7 @@ function createCauthy(given = {}) {
|
|
|
3556
3274
|
return json({ status: "added", member: orgMemberOf(res.data) }, 200, mine.headers);
|
|
3557
3275
|
}
|
|
3558
3276
|
const user = typeof b.userId === "string" ? b.userId : "";
|
|
3559
|
-
if (!
|
|
3277
|
+
if (!UUID2.test(user)) return problem(400, "invalid_member", "Say which member.", void 0, mine.headers);
|
|
3560
3278
|
if (route === "/me/organizations/members/role") {
|
|
3561
3279
|
const role = typeof b.role === "string" ? b.role : "";
|
|
3562
3280
|
if (role === "") return problem(400, "invalid_role", "Choose a role.", "role", mine.headers);
|
|
@@ -3571,7 +3289,7 @@ function createCauthy(given = {}) {
|
|
|
3571
3289
|
return json({ status: "removed", userId: user }, 200, mine.headers);
|
|
3572
3290
|
}
|
|
3573
3291
|
if (route === "/me/organizations/members/invite") {
|
|
3574
|
-
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);
|
|
3575
3293
|
if (res.error) return refused(res);
|
|
3576
3294
|
return json({ status: "sent", userId: user }, 200, mine.headers);
|
|
3577
3295
|
}
|
|
@@ -3689,18 +3407,15 @@ function createCauthy(given = {}) {
|
|
|
3689
3407
|
const url = new URL(request.url);
|
|
3690
3408
|
const inside = url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
|
|
3691
3409
|
const route = inside ? withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/" : null;
|
|
3692
|
-
const published3 = pact !== void 0 && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD");
|
|
3693
3410
|
if (identity && route !== null) {
|
|
3694
3411
|
const unready = await identityGate();
|
|
3695
3412
|
if (unready) return finish(unready, route);
|
|
3696
3413
|
}
|
|
3697
3414
|
try {
|
|
3698
|
-
return finish(await serve(request), route
|
|
3415
|
+
return finish(await serve(request), route);
|
|
3699
3416
|
} catch (err) {
|
|
3700
3417
|
if (err instanceof UnknownOriginError) return finish(problem(400, "unknown_origin", err.message), route);
|
|
3701
|
-
|
|
3702
|
-
const wantsPage = request.method === "GET" && (request.headers.get("sec-fetch-mode") ?? "navigate") === "navigate";
|
|
3703
|
-
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;
|
|
3704
3419
|
}
|
|
3705
3420
|
}
|
|
3706
3421
|
async function identityGate() {
|
|
@@ -3724,7 +3439,6 @@ function createCauthy(given = {}) {
|
|
|
3724
3439
|
}
|
|
3725
3440
|
async function serve(request) {
|
|
3726
3441
|
const url = new URL(request.url);
|
|
3727
|
-
if (pact && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD")) return pact.jwks();
|
|
3728
3442
|
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
|
|
3729
3443
|
const route = withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/";
|
|
3730
3444
|
const get = request.method === "GET" || request.method === "HEAD";
|
|
@@ -3748,7 +3462,7 @@ function createCauthy(given = {}) {
|
|
|
3748
3462
|
return redirect(back, r.cookies);
|
|
3749
3463
|
}
|
|
3750
3464
|
if (route === "/account" && get) {
|
|
3751
|
-
const q = new URLSearchParams({ client_id:
|
|
3465
|
+
const q = new URLSearchParams({ client_id: signInClientNow() });
|
|
3752
3466
|
const back = localPath(url.searchParams.get("return_to"), "");
|
|
3753
3467
|
if (back !== "") q.set("return_to", `${originOf(request)}${back}`);
|
|
3754
3468
|
const locale = url.searchParams.get("locale");
|
|
@@ -3767,7 +3481,7 @@ function createCauthy(given = {}) {
|
|
|
3767
3481
|
const asker = await signedInAs(request);
|
|
3768
3482
|
if ("response" in asker) return asker.response;
|
|
3769
3483
|
const org = typeof b.organizationId === "string" ? b.organizationId : "";
|
|
3770
|
-
if (!
|
|
3484
|
+
if (!UUID2.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
|
|
3771
3485
|
const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
|
|
3772
3486
|
if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
|
|
3773
3487
|
const attributes2 = b.attributes;
|
|
@@ -3790,8 +3504,7 @@ function createCauthy(given = {}) {
|
|
|
3790
3504
|
}
|
|
3791
3505
|
}
|
|
3792
3506
|
async function protect(request) {
|
|
3793
|
-
const
|
|
3794
|
-
const value = read1.value !== null && unverified(read1.value.u) ? null : read1.value;
|
|
3507
|
+
const { value } = await read(request);
|
|
3795
3508
|
if (value !== null && fresh(value)) return { session: toSession(value) };
|
|
3796
3509
|
const url = new URL(request.url);
|
|
3797
3510
|
const state = value === null ? "none" : "expired";
|
|
@@ -3804,85 +3517,36 @@ function createCauthy(given = {}) {
|
|
|
3804
3517
|
return { response: redirect(to) };
|
|
3805
3518
|
}
|
|
3806
3519
|
const replay = given.webhookReplay ?? memoryReplayStore();
|
|
3807
|
-
let lateSelf;
|
|
3808
3520
|
const tokenVerifier = (o = {}) => {
|
|
3809
|
-
if (
|
|
3810
|
-
if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
|
|
3811
|
-
throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
|
|
3812
|
-
}
|
|
3521
|
+
if (identity) {
|
|
3813
3522
|
let built;
|
|
3814
3523
|
return async (from) => {
|
|
3815
|
-
const id = await
|
|
3524
|
+
const id = await identity.ready();
|
|
3816
3525
|
if (built?.from !== id) {
|
|
3817
|
-
built = { from: id, verify: createTokenVerifier({
|
|
3818
|
-
issuer,
|
|
3819
|
-
tenantId: contract?.tenantId ?? id.tenant.id,
|
|
3820
|
-
audience: id.audience,
|
|
3821
|
-
fetch: given.fetch,
|
|
3822
|
-
...contract ? { clientId: contract.clientId } : {},
|
|
3823
|
-
...o
|
|
3824
|
-
}) };
|
|
3526
|
+
built = { from: id, verify: createTokenVerifier({ issuer, tenantId: id.tenant.id, audience: id.audience, fetch: given.fetch, ...o }) };
|
|
3825
3527
|
}
|
|
3826
3528
|
return built.verify(from);
|
|
3827
3529
|
};
|
|
3828
3530
|
}
|
|
3829
|
-
if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
|
|
3830
|
-
throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
|
|
3831
|
-
}
|
|
3832
3531
|
return createTokenVerifier({
|
|
3833
3532
|
issuer,
|
|
3834
|
-
tenantId:
|
|
3533
|
+
tenantId: given.tenantId,
|
|
3835
3534
|
audience: given.audience,
|
|
3836
3535
|
fetch: given.fetch,
|
|
3837
|
-
...contract ? { clientId: contract.clientId } : {},
|
|
3838
3536
|
...o
|
|
3839
3537
|
});
|
|
3840
3538
|
};
|
|
3841
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 });
|
|
3842
|
-
async function keyJwks() {
|
|
3843
|
-
const s = await fromKey.signer;
|
|
3844
|
-
const keys2 = [{ ...s.publicJwk, kid: s.kid, alg: "EdDSA", use: "sig" }];
|
|
3845
|
-
if (fromKey.previousJwk) keys2.push({ ...await fromKey.previousJwk, alg: "EdDSA", use: "sig" });
|
|
3846
|
-
if (ownKeys) keys2.push(...(await ownKeys.published()).keys);
|
|
3847
|
-
return new Response(JSON.stringify({ keys: keys2 }), {
|
|
3848
|
-
headers: { "content-type": "application/json; charset=utf-8", "cache-control": `public, max-age=${JWKS_MAX_AGE_S}` }
|
|
3849
|
-
});
|
|
3850
|
-
}
|
|
3851
|
-
const noStore = () => {
|
|
3852
|
-
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");
|
|
3853
|
-
};
|
|
3854
|
-
const keyOnly = {
|
|
3855
|
-
current: async () => noStore(),
|
|
3856
|
-
published: async () => ({ keys: [] }),
|
|
3857
|
-
rotate: async () => noStore(),
|
|
3858
|
-
reseal: async () => 0
|
|
3859
|
-
};
|
|
3860
|
-
const pact = contract && fromKey ? {
|
|
3861
|
-
contract,
|
|
3862
|
-
keys: ownKeys ?? keyOnly,
|
|
3863
|
-
jwks: keyJwks,
|
|
3864
|
-
status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
|
|
3865
|
-
} : contract && ownKeys ? {
|
|
3866
|
-
contract,
|
|
3867
|
-
keys: ownKeys,
|
|
3868
|
-
async jwks() {
|
|
3869
|
-
return new Response(JSON.stringify(await ownKeys.published()), {
|
|
3870
|
-
headers: { "content-type": "application/json; charset=utf-8", "cache-control": `public, max-age=${JWKS_MAX_AGE_S}` }
|
|
3871
|
-
});
|
|
3872
|
-
},
|
|
3873
|
-
status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
|
|
3874
|
-
} : void 0;
|
|
3875
3540
|
const configured = () => ({
|
|
3876
3541
|
issuer,
|
|
3877
3542
|
audience: given.audience ?? env3("CAUTHY_AUDIENCE") ?? "",
|
|
3878
|
-
tenant: { id:
|
|
3543
|
+
tenant: { id: given.tenantId ?? env3("CAUTHY_TENANT_ID") ?? "", slug: tenant },
|
|
3879
3544
|
system,
|
|
3880
3545
|
client_id: clientId,
|
|
3881
3546
|
sign_in_client_id: clientId,
|
|
3882
3547
|
app_urls: baseURL ? [withoutTrailingSlashes(baseURL)] : [],
|
|
3883
3548
|
callback_path: `${basePath}/callback`,
|
|
3884
|
-
key: null
|
|
3885
|
-
contract: contract ? { link: contract.link, system_proof: contract.systemProof, require_verified_email: contract.requireVerifiedEmail } : null
|
|
3549
|
+
key: null
|
|
3886
3550
|
});
|
|
3887
3551
|
return {
|
|
3888
3552
|
handler,
|
|
@@ -3904,7 +3568,6 @@ function createCauthy(given = {}) {
|
|
|
3904
3568
|
check,
|
|
3905
3569
|
toPublic,
|
|
3906
3570
|
urls,
|
|
3907
|
-
...pact ? { pact } : {},
|
|
3908
3571
|
options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
|
|
3909
3572
|
};
|
|
3910
3573
|
}
|
|
@@ -3988,7 +3651,6 @@ export {
|
|
|
3988
3651
|
KEY_REPLACES,
|
|
3989
3652
|
MIN_PASSWORD,
|
|
3990
3653
|
PROTOCOL_VERSION,
|
|
3991
|
-
STANDARD_VARIABLES,
|
|
3992
3654
|
can,
|
|
3993
3655
|
checkCode,
|
|
3994
3656
|
checkEmail,
|
|
@@ -4004,7 +3666,6 @@ export {
|
|
|
4004
3666
|
definePolicy,
|
|
4005
3667
|
describePasswordPolicy,
|
|
4006
3668
|
explainPasswordProblems,
|
|
4007
|
-
failClosed,
|
|
4008
3669
|
memoryReplayStore,
|
|
4009
3670
|
parseKey,
|
|
4010
3671
|
passwordPolicyOf,
|
|
@@ -4015,7 +3676,6 @@ export {
|
|
|
4015
3676
|
pushCatalog,
|
|
4016
3677
|
pushPolicy,
|
|
4017
3678
|
pushTheme,
|
|
4018
|
-
readContract,
|
|
4019
3679
|
remoteKeys,
|
|
4020
3680
|
themeFromStackmyth,
|
|
4021
3681
|
tokensFromStackmyth,
|