@maudecode/kanvil 0.1.2 → 0.2.1
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/apps/server/dist/{auth-BscXb1NQ.mjs → auth-I1XEr0d4.mjs} +272 -7
- package/apps/server/dist/{config-Qxr_7oqZ.mjs → config-CbNqtJEj.mjs} +8 -2
- package/apps/server/dist/index.mjs +29 -190
- package/apps/server/dist/migrate.mjs +1 -1
- package/apps/server/dist/recover.mjs +21 -6
- package/apps/server/drizzle/0013_authentication_policy.sql +2 -0
- package/apps/server/drizzle/meta/0013_snapshot.json +4240 -0
- package/apps/server/drizzle/meta/_journal.json +7 -0
- package/apps/web/dist/assets/{CompositeItem-D9KvUNiq.js → CompositeItem-BWPJ5Mol.js} +1 -1
- package/apps/web/dist/assets/admin-settings-DZqkC6ju.js +1 -0
- package/apps/web/dist/assets/{date-picker-lhXxKIlN.js → date-picker-6H_iwP6t.js} +1 -1
- package/apps/web/dist/assets/{ellipsis-CN74O_EP.js → ellipsis-BT172Uvu.js} +1 -1
- package/apps/web/dist/assets/{forms-456UbfIC.js → forms-B4Vg3CPz.js} +23 -23
- package/apps/web/dist/assets/image-edit-trigger-DGmVtWLC.js +1 -0
- package/apps/web/dist/assets/index-C_a21taR.js +50 -0
- package/apps/web/dist/assets/index-DM-D_3Sv.css +2 -0
- package/apps/web/dist/assets/kbd-tTQGZxaK.js +1 -0
- package/apps/web/dist/assets/link-DoIP9KJ6.js +2 -0
- package/apps/web/dist/assets/{list-order-1UoOT5nR.js → list-order-Bzsrihyr.js} +1 -1
- package/apps/web/dist/assets/member-directory-BiCE8DRq.js +1 -0
- package/apps/web/dist/assets/popover-BhzZ0U1d.js +1 -0
- package/apps/web/dist/assets/project-label-provider-9L7Uts3_.js +1 -0
- package/apps/web/dist/assets/project-settings-Q7Q9Reec.js +1 -0
- package/apps/web/dist/assets/{rotate-ccw-BLeWpuu1.js → rotate-ccw-sjyLwLLy.js} +1 -1
- package/apps/web/dist/assets/settings-B6qD6q1J.js +1 -0
- package/apps/web/dist/assets/settings-layout-B-AwUIMu.js +1 -0
- package/apps/web/dist/assets/switch-D0nFHj6l.js +1 -0
- package/apps/web/dist/assets/{table-BQbZG4BK.js → table-CYZiwo9g.js} +1 -1
- package/apps/web/dist/assets/ticket-detail-B-3vbHSB.js +2 -0
- package/apps/web/dist/assets/ticket-editor-C3d8TZIT.js +1 -0
- package/apps/web/dist/assets/ticket-list-LtGGOnwN.js +5 -0
- package/apps/web/dist/assets/ticket-tags-2rdmkcJV.js +1 -0
- package/apps/web/dist/assets/{upload-5Wg5ou_Z.js → upload-DXPRzAaa.js} +1 -1
- package/apps/web/dist/assets/useNavigate-DFohfUD3.js +1 -0
- package/apps/web/dist/assets/workspace-settings-D3tCm1vH.js +1 -0
- package/apps/web/dist/index.html +8 -4
- package/docs/api.md +4 -0
- package/docs/releasing.md +4 -2
- package/docs/self-hosting.md +9 -1
- package/package.json +2 -1
- package/apps/web/dist/assets/admin-settings-B0u2-aAK.js +0 -1
- package/apps/web/dist/assets/camera-Cv5wKkKn.js +0 -1
- package/apps/web/dist/assets/index-CVzq9BMC.css +0 -2
- package/apps/web/dist/assets/index-Dt8wYIVA.js +0 -51
- package/apps/web/dist/assets/instance-BCNvOvYa.js +0 -1
- package/apps/web/dist/assets/member-directory-C81ugis6.js +0 -1
- package/apps/web/dist/assets/popover-BWk65ByY.js +0 -1
- package/apps/web/dist/assets/project-settings-DnpVfVf5.js +0 -1
- package/apps/web/dist/assets/settings-DUXuOgWn.js +0 -1
- package/apps/web/dist/assets/settings-layout-xR4hrfte.js +0 -1
- package/apps/web/dist/assets/ticket-detail-B9M_eaVu.js +0 -2
- package/apps/web/dist/assets/ticket-editor-Chg1lDuU.js +0 -1
- package/apps/web/dist/assets/ticket-list-C8500tXj.js +0 -5
- package/apps/web/dist/assets/ticket-tags-cbwQ93Gt.js +0 -1
- package/apps/web/dist/assets/workspace-settings-Dte44Kkx.js +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as instanceUpdateSchema, c as s3SettingsSchema, d as createDatabase, f as account, g as user, h as session, i as brokerSettingsSchema, n as adminInstanceSchema, o as photoInputSchema, p as apikey, s as photoResultSchema, t as readConfig, u as searchSettingsSchema } from "./config-CbNqtJEj.mjs";
|
|
2
2
|
import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
|
|
3
|
-
import { A as
|
|
3
|
+
import { A as pushDevices, B as memberSchema, D as profilePhotos, E as notifications, F as workspaces, G as pushDeviceInput, H as notificationPreferencesSchema, I as ApiError, J as matchesOIDCBinding, K as roles, L as errorSchema, M as subscriptions, N as tickets, O as projects, P as workspaceMembers, R as hasAdminRole, S as events, T as notificationPreferences, U as okSchema, V as notificationPreferencesInput, W as operations, _ as attachmentContent, a as fail, b as deliveryCheckpoints, c as requireRow, d as createTicketIndex, f as createBlobStorage, g as createPollingJob, h as createObjectStore, i as createDomain, j as storageBackends, k as pushDeliveries, l as projectRoleSQL, m as imageResponse, n as createInstance, o as json, p as decodeImage, q as scopes, r as assertVersion, s as one, t as createAuth, u as createTicketSearch, v as attachments, w as members, x as eventClock, y as blobObjects, z as isOperation } from "./auth-I1XEr0d4.mjs";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import pg from "pg";
|
|
6
6
|
import { and, asc, count, desc, eq, gt, ilike, inArray, isNull, lte, or, sql } from "drizzle-orm";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
9
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
10
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { connect } from "amqplib";
|
|
@@ -88,7 +88,7 @@ function authPath(request) {
|
|
|
88
88
|
return fail(400, "invalid_path", "The authentication path is invalid");
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
function createAccountAdministration(database, auth) {
|
|
91
|
+
function createAccountAdministration(database, auth, config) {
|
|
92
92
|
async function revokeAfterPasswordChange(headers, userId) {
|
|
93
93
|
try {
|
|
94
94
|
await auth.api.revokeUserSessions({
|
|
@@ -99,16 +99,23 @@ function createAccountAdministration(database, auth) {
|
|
|
99
99
|
fail(503, "session_revocation_failed", "The password changed, but existing sessions could not be revoked. Revoke this user’s sessions before continuing.");
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
async function mutate(request, run) {
|
|
102
|
+
async function mutate(request, run, administratorOnly = true) {
|
|
103
103
|
const login = await auth.api.getSession({ headers: request.headers });
|
|
104
104
|
if (!login) fail(401, "unauthorized", "Sign in to continue");
|
|
105
|
-
if (!hasAdminRole(login.user.role) || login.user.banned) fail(403, "forbidden", "Instance administrator access is required");
|
|
105
|
+
if (administratorOnly && !hasAdminRole(login.user.role) || login.user.banned) fail(403, "forbidden", "Instance administrator access is required");
|
|
106
106
|
const lock = await database.pool.connect();
|
|
107
107
|
let locked = false;
|
|
108
108
|
try {
|
|
109
109
|
await lock.query("SELECT pg_advisory_lock(7361083)");
|
|
110
110
|
locked = true;
|
|
111
111
|
const path = authPath(request);
|
|
112
|
+
if (path === "/api/auth/unlink-account") {
|
|
113
|
+
if (!one((await lock.query("SELECT email_password_enabled AS enabled FROM instance_settings WHERE id=1")).rows).enabled) {
|
|
114
|
+
const accountId = z.string().parse(mutationBody.parse(await request.clone().json()).accountId);
|
|
115
|
+
const issuer = (await auth.$context).socialProviders.find((provider) => provider.id === "oidc")?.issuer;
|
|
116
|
+
if (!(await lock.query(`SELECT account_id AS "accountId", id_token AS "idToken" FROM account WHERE user_id=$1 AND provider_id='oidc' AND id<>$2`, [login.user.id, accountId])).rows.some((binding) => matchesOIDCBinding(binding, issuer, config.OIDC_CLIENT_ID))) fail(409, "oidc_binding_required", "Enable password sign-in before unlinking your last account for the configured OIDC provider");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
112
119
|
let passwordUserId;
|
|
113
120
|
if (path.endsWith("/admin/set-user-password")) passwordUserId = mutationBody.parse(await request.clone().json()).userId;
|
|
114
121
|
if ([
|
|
@@ -152,7 +159,8 @@ function createAccountAdministration(database, auth) {
|
|
|
152
159
|
"remove-user",
|
|
153
160
|
"set-user-password"
|
|
154
161
|
].some((name) => path === `/api/auth/admin/${name}`);
|
|
155
|
-
|
|
162
|
+
const unlink = path === "/api/auth/unlink-account";
|
|
163
|
+
return request.method === "POST" && (guarded || unlink) ? mutate(request, () => auth.handler(request), !unlink) : auth.handler(request);
|
|
156
164
|
},
|
|
157
165
|
register(app) {
|
|
158
166
|
app.openAPIRegistry.registerPath({
|
|
@@ -241,181 +249,6 @@ function createAccountAdministration(database, auth) {
|
|
|
241
249
|
};
|
|
242
250
|
}
|
|
243
251
|
//#endregion
|
|
244
|
-
//#region src/instance.ts
|
|
245
|
-
function storageIdentity(settings) {
|
|
246
|
-
return createHash("sha256").update(JSON.stringify([
|
|
247
|
-
settings.endpoint,
|
|
248
|
-
settings.region,
|
|
249
|
-
settings.bucket,
|
|
250
|
-
settings.prefix,
|
|
251
|
-
settings.forcePathStyle
|
|
252
|
-
])).digest("hex");
|
|
253
|
-
}
|
|
254
|
-
function createInstance(database, config) {
|
|
255
|
-
const key = createHash("sha256").update("kanvil:instance-config:v1:").update(config.BETTER_AUTH_SECRET).digest();
|
|
256
|
-
function seal(value) {
|
|
257
|
-
const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
258
|
-
const bytes = Buffer.concat([cipher.update(JSON.stringify(value)), cipher.final()]);
|
|
259
|
-
return Buffer.concat([
|
|
260
|
-
iv,
|
|
261
|
-
cipher.getAuthTag(),
|
|
262
|
-
bytes
|
|
263
|
-
]).toString("base64");
|
|
264
|
-
}
|
|
265
|
-
function unseal(value) {
|
|
266
|
-
try {
|
|
267
|
-
const bytes = Buffer.from(value, "base64"), decipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(0, 12));
|
|
268
|
-
decipher.setAuthTag(bytes.subarray(12, 28));
|
|
269
|
-
return JSON.parse(Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString());
|
|
270
|
-
} catch {
|
|
271
|
-
return fail(503, "configuration_key_changed", "Saved integration credentials cannot be decrypted. Restore the instance secret from your backup.");
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async function read(db = database.db) {
|
|
275
|
-
await db.insert(instanceSettings).values({ id: 1 }).onConflictDoNothing();
|
|
276
|
-
return one(await db.select().from(instanceSettings).where(eq(instanceSettings.id, 1)));
|
|
277
|
-
}
|
|
278
|
-
async function backend(id, db = database.db) {
|
|
279
|
-
const row = one(await db.select().from(storageBackends).where(eq(storageBackends.id, id)));
|
|
280
|
-
const settings = s3SettingsSchema.parse(unseal(row.config));
|
|
281
|
-
return {
|
|
282
|
-
...row,
|
|
283
|
-
settings,
|
|
284
|
-
spec: {
|
|
285
|
-
provider: "s3",
|
|
286
|
-
config: settings
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
async function resolveS3(settings, db = database.db) {
|
|
291
|
-
settings = s3SettingsSchema.strip().parse(settings);
|
|
292
|
-
const identity = storageIdentity(settings);
|
|
293
|
-
const previous = (await db.select().from(storageBackends).where(eq(storageBackends.identity, identity)))[0];
|
|
294
|
-
const old = previous ? s3SettingsSchema.parse(unseal(previous.config)) : void 0;
|
|
295
|
-
const { secretAccessKey: suppliedSecret, ...publicSettings } = settings, secret = suppliedSecret || (old?.accessKeyId === settings.accessKeyId ? old.secretAccessKey : void 0);
|
|
296
|
-
if (settings.accessKeyId && !secret) fail(400, "missing_secret", "Provide the S3 secret access key");
|
|
297
|
-
return {
|
|
298
|
-
...publicSettings,
|
|
299
|
-
...secret ? { secretAccessKey: secret } : {}
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
async function resolveSearch(settings, db = database.db) {
|
|
303
|
-
settings = searchSettingsSchema.strip().parse(settings);
|
|
304
|
-
const row = await read(db), old = row.searchConfig ? searchSettingsSchema.parse(unseal(row.searchConfig)) : null, same = old?.url === settings.url && old.index === settings.index;
|
|
305
|
-
const { apiKey: suppliedKey, password: suppliedPassword, auth: requestedAuth, ...publicSettings } = settings, auth = requestedAuth ?? (suppliedKey ? "apiKey" : suppliedPassword && settings.username ? "basic" : same && old ? searchAuthentication(old) : searchAuthentication(settings)), apiKey = suppliedKey || (same ? old?.apiKey : void 0), password = suppliedPassword || (same && old?.username === settings.username ? old?.password : void 0);
|
|
306
|
-
if (auth === "apiKey" && !apiKey) fail(400, "missing_secret", "Provide the search API key");
|
|
307
|
-
if (auth === "basic" && (!settings.username || !password)) fail(400, "missing_secret", "Provide the search username and password");
|
|
308
|
-
return {
|
|
309
|
-
...publicSettings,
|
|
310
|
-
auth,
|
|
311
|
-
...auth === "apiKey" && apiKey ? { apiKey } : {},
|
|
312
|
-
...auth === "basic" && password ? { password } : {}
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
async function saveBackend(settings, db = database.db) {
|
|
316
|
-
const complete = await resolveS3(settings, db), identity = storageIdentity(settings);
|
|
317
|
-
return one(await db.insert(storageBackends).values({
|
|
318
|
-
identity,
|
|
319
|
-
config: seal(complete)
|
|
320
|
-
}).onConflictDoUpdate({
|
|
321
|
-
target: storageBackends.identity,
|
|
322
|
-
set: { config: seal(complete) }
|
|
323
|
-
}).returning());
|
|
324
|
-
}
|
|
325
|
-
async function storage() {
|
|
326
|
-
if (config.KANVIL_STORAGE_PROVIDER === "local") return null;
|
|
327
|
-
if (config.KANVIL_STORAGE_PROVIDER === "s3") return saveBackend(s3SettingsSchema.parse({
|
|
328
|
-
endpoint: config.S3_ENDPOINT ?? "",
|
|
329
|
-
region: config.S3_REGION,
|
|
330
|
-
bucket: config.S3_BUCKET,
|
|
331
|
-
prefix: config.S3_PREFIX,
|
|
332
|
-
forcePathStyle: config.S3_FORCE_PATH_STYLE,
|
|
333
|
-
accessKeyId: config.S3_ACCESS_KEY_ID ?? "",
|
|
334
|
-
secretAccessKey: config.S3_SECRET_ACCESS_KEY
|
|
335
|
-
}));
|
|
336
|
-
const row = await read();
|
|
337
|
-
return row.storageBackendId ? backend(row.storageBackendId) : null;
|
|
338
|
-
}
|
|
339
|
-
async function search() {
|
|
340
|
-
const row = await read();
|
|
341
|
-
if (config.KANVIL_SEARCH_PROVIDER === "postgres") return null;
|
|
342
|
-
if (config.KANVIL_SEARCH_PROVIDER === "elasticsearch") {
|
|
343
|
-
const settings = searchSettingsSchema.parse({
|
|
344
|
-
url: config.ELASTICSEARCH_URL,
|
|
345
|
-
index: config.ELASTICSEARCH_INDEX ?? `kanvil-${row.identity}`,
|
|
346
|
-
auth: config.ELASTICSEARCH_AUTH,
|
|
347
|
-
apiKey: config.ELASTICSEARCH_API_KEY,
|
|
348
|
-
username: config.ELASTICSEARCH_USERNAME,
|
|
349
|
-
password: config.ELASTICSEARCH_PASSWORD
|
|
350
|
-
});
|
|
351
|
-
return {
|
|
352
|
-
...settings,
|
|
353
|
-
auth: searchAuthentication(settings)
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
const settings = row.searchConfig ? searchSettingsSchema.parse(unseal(row.searchConfig)) : null;
|
|
357
|
-
return settings ? {
|
|
358
|
-
...settings,
|
|
359
|
-
auth: searchAuthentication(settings)
|
|
360
|
-
} : null;
|
|
361
|
-
}
|
|
362
|
-
async function broker() {
|
|
363
|
-
if (config.KANVIL_BROKER_PROVIDER === "postgres") return null;
|
|
364
|
-
if (config.KANVIL_BROKER_PROVIDER === "rabbitmq") return brokerSettingsSchema.parse({
|
|
365
|
-
url: config.RABBITMQ_URL,
|
|
366
|
-
exchange: config.RABBITMQ_EXCHANGE
|
|
367
|
-
});
|
|
368
|
-
const row = await read();
|
|
369
|
-
return row.brokerConfig ? brokerSettingsSchema.parse(unseal(row.brokerConfig)) : null;
|
|
370
|
-
}
|
|
371
|
-
async function resolveBroker(settings, db = database.db) {
|
|
372
|
-
settings = brokerSettingsSchema.strip().parse(settings);
|
|
373
|
-
const row = await read(db), old = row.brokerConfig ? brokerSettingsSchema.parse(unseal(row.brokerConfig)) : null;
|
|
374
|
-
const url = settings.url || old?.url;
|
|
375
|
-
if (!url) return fail(400, "missing_secret", "Provide the RabbitMQ connection URL");
|
|
376
|
-
return {
|
|
377
|
-
...settings,
|
|
378
|
-
url
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
async function update(value) {
|
|
382
|
-
const input = instanceUpdateSchema.parse(value);
|
|
383
|
-
return database.db.transaction(async (tx) => {
|
|
384
|
-
await tx.execute(sql`SELECT pg_advisory_xact_lock(7361083)`);
|
|
385
|
-
const current = await read(tx);
|
|
386
|
-
assertVersion(current, input.expectedVersion, "instance settings", "saving");
|
|
387
|
-
let storageId = current.storageBackendId, searchConfig = current.searchConfig, brokerConfig = current.brokerConfig;
|
|
388
|
-
if (!config.KANVIL_STORAGE_PROVIDER) storageId = input.storage.provider === "s3" ? (await saveBackend(input.storage, tx)).id : null;
|
|
389
|
-
if (!config.KANVIL_SEARCH_PROVIDER) {
|
|
390
|
-
if (input.search.provider === "postgres") searchConfig = null;
|
|
391
|
-
else searchConfig = seal(await resolveSearch(input.search, tx));
|
|
392
|
-
}
|
|
393
|
-
if (!config.KANVIL_BROKER_PROVIDER && input.broker) brokerConfig = input.broker.provider === "postgres" ? null : seal(await resolveBroker(input.broker, tx));
|
|
394
|
-
await tx.update(instanceSettings).set({
|
|
395
|
-
name: input.name,
|
|
396
|
-
description: input.description,
|
|
397
|
-
storageBackendId: storageId,
|
|
398
|
-
searchConfig,
|
|
399
|
-
brokerConfig,
|
|
400
|
-
version: current.version + 1
|
|
401
|
-
}).where(eq(instanceSettings.id, 1));
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
return {
|
|
405
|
-
read,
|
|
406
|
-
storage,
|
|
407
|
-
search,
|
|
408
|
-
backend,
|
|
409
|
-
saveBackend,
|
|
410
|
-
resolveS3,
|
|
411
|
-
resolveSearch,
|
|
412
|
-
broker,
|
|
413
|
-
resolveBroker,
|
|
414
|
-
update,
|
|
415
|
-
config
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
//#endregion
|
|
419
252
|
//#region src/access.ts
|
|
420
253
|
const credentialPermissions = z.object({ kanvil: z.array(z.enum(scopes)) });
|
|
421
254
|
const credentialMetadata = z.object({ projectIds: z.array(z.uuid()).max(1e3) });
|
|
@@ -497,6 +330,9 @@ function createAccess(auth, domain, config, database) {
|
|
|
497
330
|
};
|
|
498
331
|
}
|
|
499
332
|
//#endregion
|
|
333
|
+
//#region src/version.ts
|
|
334
|
+
const applicationVersion = "0.2.1";
|
|
335
|
+
//#endregion
|
|
500
336
|
//#region src/realtime.ts
|
|
501
337
|
async function createRealtime(config) {
|
|
502
338
|
const emitter = new EventEmitter();
|
|
@@ -815,7 +651,7 @@ async function createEventDelivery(config, instance, database, factory = createW
|
|
|
815
651
|
}
|
|
816
652
|
//#endregion
|
|
817
653
|
//#region src/instance-routes.ts
|
|
818
|
-
function registerInstanceRoutes(app, database, instance, blobs, search, delivery) {
|
|
654
|
+
function registerInstanceRoutes(app, database, instance, blobs, search, delivery, auth) {
|
|
819
655
|
app.openAPIRegistry.registerComponent("securitySchemes", "sessionCookie", {
|
|
820
656
|
type: "apiKey",
|
|
821
657
|
in: "cookie",
|
|
@@ -922,6 +758,7 @@ function registerInstanceRoutes(app, database, instance, blobs, search, delivery
|
|
|
922
758
|
return Response.json({
|
|
923
759
|
id: row.identity,
|
|
924
760
|
version: row.version,
|
|
761
|
+
authentication: await instance.authentication(),
|
|
925
762
|
name: row.name,
|
|
926
763
|
description: row.description,
|
|
927
764
|
storage: {
|
|
@@ -957,7 +794,7 @@ function registerInstanceRoutes(app, database, instance, blobs, search, delivery
|
|
|
957
794
|
},
|
|
958
795
|
deployment: {
|
|
959
796
|
url: instance.config.KANVIL_URL,
|
|
960
|
-
version:
|
|
797
|
+
version: applicationVersion,
|
|
961
798
|
oidc: instance.config.OIDC_DISCOVERY_URL ? {
|
|
962
799
|
name: instance.config.OIDC_NAME,
|
|
963
800
|
discoveryUrl: instance.config.OIDC_DISCOVERY_URL,
|
|
@@ -995,7 +832,8 @@ function registerInstanceRoutes(app, database, instance, blobs, search, delivery
|
|
|
995
832
|
const candidate = await instance.resolveBroker(input.broker);
|
|
996
833
|
if (!isDeepStrictEqual(candidate, await instance.broker())) await delivery.test(candidate);
|
|
997
834
|
}
|
|
998
|
-
await
|
|
835
|
+
const issuer = input.authentication && !input.authentication.emailPasswordEnabled ? (await auth.$context).socialProviders.find((provider) => provider.id === "oidc")?.issuer : void 0;
|
|
836
|
+
await instance.update(input, c.get("actor").id, issuer);
|
|
999
837
|
return Response.json({ ok: true });
|
|
1000
838
|
});
|
|
1001
839
|
app.post("/api/v1/admin/storage/test", async (c) => {
|
|
@@ -1178,7 +1016,7 @@ function createMcp(auth, domain, access, config, limitWrite) {
|
|
|
1178
1016
|
const serve = (request, identity, limited = false) => createMcpHandler(() => {
|
|
1179
1017
|
const server = new McpServer({
|
|
1180
1018
|
name: "Kanvil",
|
|
1181
|
-
version:
|
|
1019
|
+
version: applicationVersion
|
|
1182
1020
|
});
|
|
1183
1021
|
for (const name of Object.keys(operations).filter(isOperation)) {
|
|
1184
1022
|
const spec = operations[name];
|
|
@@ -1269,7 +1107,7 @@ async function createApplication(database, config, overrides = {}) {
|
|
|
1269
1107
|
search.sync().catch(() => {});
|
|
1270
1108
|
});
|
|
1271
1109
|
const push = adapters.push(database, config);
|
|
1272
|
-
const accounts = createAccountAdministration(integrationDatabase, auth);
|
|
1110
|
+
const accounts = createAccountAdministration(integrationDatabase, auth, config);
|
|
1273
1111
|
const app = new OpenAPIHono({ defaultHook: (result) => {
|
|
1274
1112
|
if (!result.success) return Response.json({ error: {
|
|
1275
1113
|
code: "validation_error",
|
|
@@ -1337,7 +1175,8 @@ async function createApplication(database, config, overrides = {}) {
|
|
|
1337
1175
|
name: row.name,
|
|
1338
1176
|
description: row.description
|
|
1339
1177
|
})),
|
|
1340
|
-
version:
|
|
1178
|
+
version: applicationVersion,
|
|
1179
|
+
authentication: await instance.authentication(),
|
|
1341
1180
|
setupRequired: await setupRequired(),
|
|
1342
1181
|
push: config.KANVIL_PUSH_ENABLED,
|
|
1343
1182
|
oidc: config.OIDC_DISCOVERY_URL ? {
|
|
@@ -1390,7 +1229,7 @@ async function createApplication(database, config, overrides = {}) {
|
|
|
1390
1229
|
requireInstanceAdmin(c.get("actor"));
|
|
1391
1230
|
await next();
|
|
1392
1231
|
});
|
|
1393
|
-
registerInstanceRoutes(app, database, instance, blobs, search, realtime);
|
|
1232
|
+
registerInstanceRoutes(app, database, instance, blobs, search, realtime, auth);
|
|
1394
1233
|
accounts.register(app);
|
|
1395
1234
|
app.openAPIRegistry.registerPath({
|
|
1396
1235
|
method: "get",
|
|
@@ -1674,7 +1513,7 @@ async function createApplication(database, config, overrides = {}) {
|
|
|
1674
1513
|
openapi: "3.1.0",
|
|
1675
1514
|
info: {
|
|
1676
1515
|
title: "Kanvil API",
|
|
1677
|
-
version:
|
|
1516
|
+
version: applicationVersion
|
|
1678
1517
|
},
|
|
1679
1518
|
servers: [{ url: config.KANVIL_URL }]
|
|
1680
1519
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as createDatabase, t as readConfig } from "./config-CbNqtJEj.mjs";
|
|
2
2
|
import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
|
|
3
3
|
//#region src/cli/migrate.ts
|
|
4
4
|
const database = createDatabase(readConfig().DATABASE_URL);
|
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import { d as account,
|
|
2
|
-
import {
|
|
3
|
-
import { and, eq } from "drizzle-orm";
|
|
1
|
+
import { d as createDatabase, f as account, g as user, h as session, t as readConfig } from "./config-CbNqtJEj.mjs";
|
|
2
|
+
import { C as instanceSettings, R as hasAdminRole, a as fail, t as createAuth } from "./auth-I1XEr0d4.mjs";
|
|
3
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import passwordPrompt from "@inquirer/password";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
//#region src/recover-account.ts
|
|
9
9
|
const localPasswordSchema = z.string().min(12).max(128);
|
|
10
|
-
async function recoverLocalPassword(database, config, email, password) {
|
|
10
|
+
async function recoverLocalPassword(database, config, email, password, enablePasswordSignIn = false) {
|
|
11
11
|
const normalizedEmail = z.email().parse(email.trim().toLowerCase());
|
|
12
12
|
localPasswordSchema.parse(password);
|
|
13
13
|
const hash = await (await createAuth(database, config).$context).password.hash(password);
|
|
14
14
|
return database.db.transaction(async (tx) => {
|
|
15
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(7361083)`);
|
|
15
16
|
const identity = (await tx.select().from(user).where(eq(user.email, normalizedEmail)).for("update"))[0];
|
|
16
17
|
if (!identity) fail(404, "not_found", "No account has this email address");
|
|
18
|
+
if (enablePasswordSignIn) {
|
|
19
|
+
if (!hasAdminRole(identity.role) || identity.banned) fail(403, "forbidden", "Recover an active administrator to enable password sign-in");
|
|
20
|
+
await tx.update(instanceSettings).set({
|
|
21
|
+
emailPasswordEnabled: true,
|
|
22
|
+
version: sql`${instanceSettings.version} + 1`
|
|
23
|
+
}).where(eq(instanceSettings.id, 1));
|
|
24
|
+
}
|
|
17
25
|
const where = and(eq(account.userId, identity.id), eq(account.providerId, "credential"));
|
|
18
26
|
if ((await tx.select().from(account).where(where))[0]) await tx.update(account).set({
|
|
19
27
|
password: hash,
|
|
@@ -33,7 +41,13 @@ async function recoverLocalPassword(database, config, email, password) {
|
|
|
33
41
|
}
|
|
34
42
|
//#endregion
|
|
35
43
|
//#region src/cli/recover.ts
|
|
36
|
-
const { values } = parseArgs({ options: {
|
|
44
|
+
const { values } = parseArgs({ options: {
|
|
45
|
+
email: { type: "string" },
|
|
46
|
+
"enable-password-sign-in": {
|
|
47
|
+
type: "boolean",
|
|
48
|
+
default: false
|
|
49
|
+
}
|
|
50
|
+
} });
|
|
37
51
|
if (!values.email) throw new Error("Pass --email <account-email>. The new password is read privately from the terminal or stdin.");
|
|
38
52
|
const password = process.stdin.isTTY ? await passwordPrompt({
|
|
39
53
|
message: "New local password",
|
|
@@ -43,7 +57,8 @@ const password = process.stdin.isTTY ? await passwordPrompt({
|
|
|
43
57
|
const config = readConfig();
|
|
44
58
|
const database = createDatabase(config.DATABASE_URL);
|
|
45
59
|
try {
|
|
46
|
-
await recoverLocalPassword(database, config, values.email, password);
|
|
60
|
+
await recoverLocalPassword(database, config, values.email, password, values["enable-password-sign-in"]);
|
|
61
|
+
if (values["enable-password-sign-in"]) console.info("Instance password sign-in enabled.");
|
|
47
62
|
console.info("Local password replaced and account sessions revoked. API credentials and linked OIDC accounts are retained.");
|
|
48
63
|
} finally {
|
|
49
64
|
await database.pool.end();
|