@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.
Files changed (55) hide show
  1. package/apps/server/dist/{auth-BscXb1NQ.mjs → auth-I1XEr0d4.mjs} +272 -7
  2. package/apps/server/dist/{config-Qxr_7oqZ.mjs → config-CbNqtJEj.mjs} +8 -2
  3. package/apps/server/dist/index.mjs +29 -190
  4. package/apps/server/dist/migrate.mjs +1 -1
  5. package/apps/server/dist/recover.mjs +21 -6
  6. package/apps/server/drizzle/0013_authentication_policy.sql +2 -0
  7. package/apps/server/drizzle/meta/0013_snapshot.json +4240 -0
  8. package/apps/server/drizzle/meta/_journal.json +7 -0
  9. package/apps/web/dist/assets/{CompositeItem-D9KvUNiq.js → CompositeItem-BWPJ5Mol.js} +1 -1
  10. package/apps/web/dist/assets/admin-settings-DZqkC6ju.js +1 -0
  11. package/apps/web/dist/assets/{date-picker-lhXxKIlN.js → date-picker-6H_iwP6t.js} +1 -1
  12. package/apps/web/dist/assets/{ellipsis-CN74O_EP.js → ellipsis-BT172Uvu.js} +1 -1
  13. package/apps/web/dist/assets/{forms-456UbfIC.js → forms-B4Vg3CPz.js} +23 -23
  14. package/apps/web/dist/assets/image-edit-trigger-DGmVtWLC.js +1 -0
  15. package/apps/web/dist/assets/index-C_a21taR.js +50 -0
  16. package/apps/web/dist/assets/index-DM-D_3Sv.css +2 -0
  17. package/apps/web/dist/assets/kbd-tTQGZxaK.js +1 -0
  18. package/apps/web/dist/assets/link-DoIP9KJ6.js +2 -0
  19. package/apps/web/dist/assets/{list-order-1UoOT5nR.js → list-order-Bzsrihyr.js} +1 -1
  20. package/apps/web/dist/assets/member-directory-BiCE8DRq.js +1 -0
  21. package/apps/web/dist/assets/popover-BhzZ0U1d.js +1 -0
  22. package/apps/web/dist/assets/project-label-provider-9L7Uts3_.js +1 -0
  23. package/apps/web/dist/assets/project-settings-Q7Q9Reec.js +1 -0
  24. package/apps/web/dist/assets/{rotate-ccw-BLeWpuu1.js → rotate-ccw-sjyLwLLy.js} +1 -1
  25. package/apps/web/dist/assets/settings-B6qD6q1J.js +1 -0
  26. package/apps/web/dist/assets/settings-layout-B-AwUIMu.js +1 -0
  27. package/apps/web/dist/assets/switch-D0nFHj6l.js +1 -0
  28. package/apps/web/dist/assets/{table-BQbZG4BK.js → table-CYZiwo9g.js} +1 -1
  29. package/apps/web/dist/assets/ticket-detail-B-3vbHSB.js +2 -0
  30. package/apps/web/dist/assets/ticket-editor-C3d8TZIT.js +1 -0
  31. package/apps/web/dist/assets/ticket-list-LtGGOnwN.js +5 -0
  32. package/apps/web/dist/assets/ticket-tags-2rdmkcJV.js +1 -0
  33. package/apps/web/dist/assets/{upload-5Wg5ou_Z.js → upload-DXPRzAaa.js} +1 -1
  34. package/apps/web/dist/assets/useNavigate-DFohfUD3.js +1 -0
  35. package/apps/web/dist/assets/workspace-settings-D3tCm1vH.js +1 -0
  36. package/apps/web/dist/index.html +8 -4
  37. package/docs/api.md +4 -0
  38. package/docs/releasing.md +4 -2
  39. package/docs/self-hosting.md +9 -1
  40. package/package.json +2 -1
  41. package/apps/web/dist/assets/admin-settings-B0u2-aAK.js +0 -1
  42. package/apps/web/dist/assets/camera-Cv5wKkKn.js +0 -1
  43. package/apps/web/dist/assets/index-CVzq9BMC.css +0 -2
  44. package/apps/web/dist/assets/index-Dt8wYIVA.js +0 -51
  45. package/apps/web/dist/assets/instance-BCNvOvYa.js +0 -1
  46. package/apps/web/dist/assets/member-directory-C81ugis6.js +0 -1
  47. package/apps/web/dist/assets/popover-BWk65ByY.js +0 -1
  48. package/apps/web/dist/assets/project-settings-DnpVfVf5.js +0 -1
  49. package/apps/web/dist/assets/settings-DUXuOgWn.js +0 -1
  50. package/apps/web/dist/assets/settings-layout-xR4hrfte.js +0 -1
  51. package/apps/web/dist/assets/ticket-detail-B9M_eaVu.js +0 -2
  52. package/apps/web/dist/assets/ticket-editor-Chg1lDuU.js +0 -1
  53. package/apps/web/dist/assets/ticket-list-C8500tXj.js +0 -5
  54. package/apps/web/dist/assets/ticket-tags-cbwQ93Gt.js +0 -1
  55. package/apps/web/dist/assets/workspace-settings-Dte44Kkx.js +0 -1
@@ -1,12 +1,12 @@
1
- import { a as photoInputSchema, c as searchAuthentication, d as account, f as apikey, h as user, i as instanceUpdateSchema, l as searchSettingsSchema, m as session, n as adminInstanceSchema, o as photoResultSchema, r as brokerSettingsSchema, s as s3SettingsSchema, t as readConfig, u as createDatabase } from "./config-Qxr_7oqZ.mjs";
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 storageBackends, B as notificationPreferencesInput, C as members, D as projects, E as profilePhotos, F as ApiError, G as roles, H as okSchema, I as errorSchema, K as scopes, L as hasAdminRole, M as tickets, N as workspaceMembers, O as pushDeliveries, P as workspaces, R as isOperation, S as instanceSettings, T as notifications, U as operations, V as notificationPreferencesSchema, W as pushDeviceInput, _ as attachments, a as json, b as eventClock, c as projectRoleSQL, d as createBlobStorage, f as decodeImage, g as attachmentContent, h as createPollingJob, i as fail, j as subscriptions, k as pushDevices, l as createTicketSearch, m as createObjectStore, n as assertVersion, o as one, p as imageResponse, r as createDomain, s as requireRow, t as createAuth, u as createTicketIndex, v as blobObjects, w as notificationPreferences, x as events, y as deliveryCheckpoints, z as memberSchema } from "./auth-BscXb1NQ.mjs";
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 { createCipheriv, createDecipheriv, createHash, randomBytes, timingSafeEqual } from "node:crypto";
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
- return request.method === "POST" && guarded ? mutate(request, () => auth.handler(request)) : auth.handler(request);
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: "0.1.0",
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 instance.update(input);
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: "0.1.0"
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: "0.1.0",
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: "0.1.0"
1516
+ version: applicationVersion
1678
1517
  },
1679
1518
  servers: [{ url: config.KANVIL_URL }]
1680
1519
  });
@@ -1,4 +1,4 @@
1
- import { t as readConfig, u as createDatabase } from "./config-Qxr_7oqZ.mjs";
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, h as user, m as session, t as readConfig, u as createDatabase } from "./config-Qxr_7oqZ.mjs";
2
- import { i as fail, t as createAuth } from "./auth-BscXb1NQ.mjs";
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: { email: { type: "string" } } });
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();
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "instance_settings" ADD COLUMN "email_password_enabled" boolean DEFAULT true NOT NULL;--> statement-breakpoint
2
+ ALTER TABLE "instance_settings" ADD COLUMN "sso_auto_provision" boolean DEFAULT false NOT NULL;