@maudecode/kanvil 0.6.1 → 0.7.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.
Files changed (124) hide show
  1. package/apps/server/dist/{auth-CFjetlB4.mjs → auth-D43XyHdK.mjs} +227 -71
  2. package/apps/server/dist/client.mjs +2 -2
  3. package/apps/server/dist/{config-BQw6mPzE.mjs → config-CQVdVhXv.mjs} +1 -1
  4. package/apps/server/dist/index.mjs +161 -44
  5. package/apps/server/dist/migrate.mjs +1 -1
  6. package/apps/server/dist/recover.mjs +3 -3
  7. package/apps/server/dist/{src-BaTNDJB7.mjs → src-D70GmZ5R.mjs} +83 -26
  8. package/apps/server/drizzle/0040_upload_urls.sql +20 -0
  9. package/apps/server/drizzle/0041_project_repositories.sql +2 -0
  10. package/apps/server/drizzle/meta/0040_snapshot.json +7170 -0
  11. package/apps/server/drizzle/meta/0041_snapshot.json +7183 -0
  12. package/apps/server/drizzle/meta/_journal.json +14 -0
  13. package/apps/web/dist/assets/{CompositeItem-Dotj8JZL.js → CompositeItem-DLTRsada.js} +1 -1
  14. package/apps/web/dist/assets/{Trans-CE4EAAY1.js → Trans-DqX-qhvQ.js} +1 -1
  15. package/apps/web/dist/assets/admin-settings-BQzdZII5.js +1 -0
  16. package/apps/web/dist/assets/{archive-restore-BMtgjFUu.js → archive-restore-BO1qng5Y.js} +1 -1
  17. package/apps/web/dist/assets/az-AZ-SXqDBPA2.js +1 -0
  18. package/apps/web/dist/assets/{az-B-DxMc__.js → az-CEW7-a7J.js} +1 -1
  19. package/apps/web/dist/assets/configuration-D0zvwrXt.js +1 -0
  20. package/apps/web/dist/assets/{corner-left-up-O4OsTW-L.js → corner-left-up-Be7Z9Uhf.js} +1 -1
  21. package/apps/web/dist/assets/{de-Btbkq-aX.js → de-B-oGVMYL.js} +1 -1
  22. package/apps/web/dist/assets/de-DE-CB7aQ6b-.js +1 -0
  23. package/apps/web/dist/assets/default-CCI1vZxa.js +56 -0
  24. package/apps/web/dist/assets/delivery-settings-BrwSrFIN.js +1 -0
  25. package/apps/web/dist/assets/{el-DBVEX16k.js → el-Cp69kezM.js} +1 -1
  26. package/apps/web/dist/assets/el-GR-D1ntYXIt.js +1 -0
  27. package/apps/web/dist/assets/{ellipsis-CvEPDi9V.js → ellipsis-B0AE8ax3.js} +1 -1
  28. package/apps/web/dist/assets/{es-CMJoxZBk.js → es-CwOMVSTx.js} +1 -1
  29. package/apps/web/dist/assets/es-ES-CllsaxcB.js +1 -0
  30. package/apps/web/dist/assets/{forms-DbfnnPg_.js → forms-CkVpTx29.js} +1 -1
  31. package/apps/web/dist/assets/{fr-OyEmYoRn.js → fr-Cy4co9ic.js} +1 -1
  32. package/apps/web/dist/assets/fr-FR-DxTw6LgI.js +1 -0
  33. package/apps/web/dist/assets/{hi-CIgXSseB.js → hi-DbO9TKwg.js} +1 -1
  34. package/apps/web/dist/assets/hi-IN-CaQga-0l.js +1 -0
  35. package/apps/web/dist/assets/{highlight-B4K3yEEh.js → highlight-zMg1BK-r.js} +1 -1
  36. package/apps/web/dist/assets/{id-WV2oSAPB.js → id-B6NKsmCm.js} +1 -1
  37. package/apps/web/dist/assets/id-ID-DBfwAAqU.js +1 -0
  38. package/apps/web/dist/assets/index-CZuFd4LM.js +17 -0
  39. package/apps/web/dist/assets/index-DPT5LAs1.css +2 -0
  40. package/apps/web/dist/assets/{invitations-BTB8f2Xl.js → invitations-Dp40BfHj.js} +1 -1
  41. package/apps/web/dist/assets/it-IT-BckuH9Nl.js +1 -0
  42. package/apps/web/dist/assets/{it-BWSrRqus.js → it-ilGYCq2T.js} +1 -1
  43. package/apps/web/dist/assets/{item-HmFuRZet.js → item-DQR89bKm.js} +1 -1
  44. package/apps/web/dist/assets/{ja-BtOHWvUJ.js → ja-D-hJRTgP.js} +1 -1
  45. package/apps/web/dist/assets/ja-JP-BAGgJKZ4.js +1 -0
  46. package/apps/web/dist/assets/ko-KR-srIEsTP5.js +1 -0
  47. package/apps/web/dist/assets/{ko-DAryK0HN.js → ko-ajkkoJ_d.js} +1 -1
  48. package/apps/web/dist/assets/{label-settings-ssfEX2uR.js → label-settings-ChnKkA97.js} +1 -1
  49. package/apps/web/dist/assets/{markdown-blocks-B1kO6iku.js → markdown-blocks-CpzrwyQG.js} +1 -1
  50. package/apps/web/dist/assets/{member-directory-BA4ZT1qU.js → member-directory-XK0ZEGUq.js} +1 -1
  51. package/apps/web/dist/assets/{mk-DBbiGUk5.js → mk-C_sMyEHX.js} +1 -1
  52. package/apps/web/dist/assets/mk-MK-BTa1NMAF.js +1 -0
  53. package/apps/web/dist/assets/{nl-D4V-8WdF.js → nl-D9-P4UPR.js} +1 -1
  54. package/apps/web/dist/assets/nl-NL-CQduhDqs.js +1 -0
  55. package/apps/web/dist/assets/{pl-C--QrxO0.js → pl-CiQM7fSB.js} +1 -1
  56. package/apps/web/dist/assets/pl-PL-BdZMf7Ni.js +1 -0
  57. package/apps/web/dist/assets/project-lifecycle-VYecJuiP.js +1 -0
  58. package/apps/web/dist/assets/project-settings-Bs6YEpo9.js +1 -0
  59. package/apps/web/dist/assets/{pt-BR-DvFVKpmO.js → pt-BR-CP7dhuqF.js} +1 -1
  60. package/apps/web/dist/assets/pt-BR-DZEXJNHg.js +1 -0
  61. package/apps/web/dist/assets/public-project-DUfWDao9.js +1 -0
  62. package/apps/web/dist/assets/{rotate-ccw-CB36Zu3L.js → rotate-ccw-Cxh9ec2s.js} +1 -1
  63. package/apps/web/dist/assets/{ru-UmbPUOka.js → ru-DxJ6vF4E.js} +1 -1
  64. package/apps/web/dist/assets/ru-RU-CZKEe7P8.js +1 -0
  65. package/apps/web/dist/assets/{search-Cvetr1OK.js → search-DfbsqO7M.js} +6 -6
  66. package/apps/web/dist/assets/{send-CJDct4Oj.js → send-DrJl0BKm.js} +1 -1
  67. package/apps/web/dist/assets/settings-BN8uIiBG.js +1 -0
  68. package/apps/web/dist/assets/{sliders-horizontal-dmqWngxy.js → sliders-horizontal-DM47MLe7.js} +1 -1
  69. package/apps/web/dist/assets/{switch-BtScTpP9.js → switch-CJTVGtE9.js} +1 -1
  70. package/apps/web/dist/assets/{table-byFokzo-.js → table-BLLjFPWD.js} +1 -1
  71. package/apps/web/dist/assets/{ticket-detail-C3YA3Hzc.js → ticket-detail-BTy_3JeU.js} +2 -2
  72. package/apps/web/dist/assets/ticket-editor-Bb_lyxnJ.js +2 -0
  73. package/apps/web/dist/assets/{ticket-list-BXUorfmj.js → ticket-list-Botp5qf5.js} +5 -5
  74. package/apps/web/dist/assets/ticket-list-controls-Dmkno4pm.js +1 -0
  75. package/apps/web/dist/assets/{tr-CNpgQ4ka.js → tr-9IkdRLIk.js} +1 -1
  76. package/apps/web/dist/assets/tr-TR-Y8wBLO8I.js +1 -0
  77. package/apps/web/dist/assets/{uk-MQoTXdpk.js → uk-Btampsdw.js} +1 -1
  78. package/apps/web/dist/assets/uk-UA-CVpasGUS.js +1 -0
  79. package/apps/web/dist/assets/{upload-D1x_ib3B.js → upload-HARy4VC_.js} +1 -1
  80. package/apps/web/dist/assets/{useQueries-BnlNCoy4.js → useQueries-CTeEF--U.js} +1 -1
  81. package/apps/web/dist/assets/{vi-CEw4gTWc.js → vi-BVZzOn_n.js} +1 -1
  82. package/apps/web/dist/assets/vi-VN-BMy-iJD-.js +1 -0
  83. package/apps/web/dist/assets/{visual-editor-CpdMRn3e.js → visual-editor-DKBPjqV-.js} +1 -1
  84. package/apps/web/dist/assets/{workspace-settings-BickBtZx.js → workspace-settings-DqzBNw60.js} +1 -1
  85. package/apps/web/dist/assets/{zh-CN-ChzoXkfr.js → zh-CN-BoUvF2do.js} +1 -1
  86. package/apps/web/dist/assets/zh-CN-Cds_v0R8.js +1 -0
  87. package/apps/web/dist/assets/{zh-TW-bWBKARrK.js → zh-TW-CEIDGTWe.js} +1 -1
  88. package/apps/web/dist/assets/zh-TW-DCCYakXz.js +1 -0
  89. package/apps/web/dist/index.html +8 -8
  90. package/docs/api.md +33 -3
  91. package/docs/cli.md +1 -1
  92. package/package.json +1 -1
  93. package/apps/web/dist/assets/admin-settings-CO7nnD9T.js +0 -1
  94. package/apps/web/dist/assets/az-AZ-R8_HFWJr.js +0 -1
  95. package/apps/web/dist/assets/configuration-Bnt72HRu.js +0 -1
  96. package/apps/web/dist/assets/de-DE-BHgMj19Y.js +0 -1
  97. package/apps/web/dist/assets/default-DqsN66dq.js +0 -56
  98. package/apps/web/dist/assets/delivery-settings-Cji0N0m0.js +0 -1
  99. package/apps/web/dist/assets/el-GR-CQFGM5Sr.js +0 -1
  100. package/apps/web/dist/assets/es-ES-DZkcRVA4.js +0 -1
  101. package/apps/web/dist/assets/fr-FR-CRPAezXU.js +0 -1
  102. package/apps/web/dist/assets/hi-IN-2YFE4UQk.js +0 -1
  103. package/apps/web/dist/assets/id-ID-B-J0_QIW.js +0 -1
  104. package/apps/web/dist/assets/index-BAnwOrh1.css +0 -2
  105. package/apps/web/dist/assets/index-nltuyQtq.js +0 -17
  106. package/apps/web/dist/assets/it-IT-CJ0HYggO.js +0 -1
  107. package/apps/web/dist/assets/ja-JP-DDQS7D7b.js +0 -1
  108. package/apps/web/dist/assets/ko-KR-CkpoewEa.js +0 -1
  109. package/apps/web/dist/assets/mk-MK-C0RhS6XR.js +0 -1
  110. package/apps/web/dist/assets/nl-NL-BSKqlzcL.js +0 -1
  111. package/apps/web/dist/assets/pl-PL-CHz38SiU.js +0 -1
  112. package/apps/web/dist/assets/project-lifecycle-COZb9FqM.js +0 -1
  113. package/apps/web/dist/assets/project-settings-DwmTm9KA.js +0 -1
  114. package/apps/web/dist/assets/pt-BR-BotWHRiO.js +0 -1
  115. package/apps/web/dist/assets/public-project-B3hEkeCF.js +0 -1
  116. package/apps/web/dist/assets/ru-RU-DtOwNna6.js +0 -1
  117. package/apps/web/dist/assets/settings-3IXm9lJ-.js +0 -1
  118. package/apps/web/dist/assets/ticket-editor-CZnZQMG1.js +0 -2
  119. package/apps/web/dist/assets/ticket-list-controls-BxGKCZMo.js +0 -1
  120. package/apps/web/dist/assets/tr-TR-CdG2alU2.js +0 -1
  121. package/apps/web/dist/assets/uk-UA-Ceu5gorw.js +0 -1
  122. package/apps/web/dist/assets/vi-VN-CxshgV4W.js +0 -1
  123. package/apps/web/dist/assets/zh-CN-D7Yrjhd3.js +0 -1
  124. package/apps/web/dist/assets/zh-TW-DyF38kqx.js +0 -1
@@ -1,11 +1,12 @@
1
- import { a as auth_schema_exports, c as oauthConsent, d as user, i as apikey, l as oauthRefreshToken, o as oauthAccessToken, r as account, s as oauthClient, u as session } from "./config-BQw6mPzE.mjs";
2
- import { $ as permissionLabels, A as hasAdminRole, At as workflowOrder, Bt as searchSettingsSchema, D as filterKey, Dt as ticketPredicates, E as eventSchema, Et as ticketImportSchema, F as isHexColor, Ft as instanceUpdateSchema, J as operationPermission, N as importSecretFields, Nt as authenticationPolicySchema, O as filterList, Ot as ticketSchema, P as invitationStatuses, Pt as brokerSettingsSchema, Q as parsePublicOutput, Rt as s3SettingsSchema, S as difficulties, St as states, T as eventLabels, Tt as ticketFilter, V as mentionScheme, X as parseCustomFieldFilter, Y as operations, Z as parseOutput, _ as deletedAccountName, _t as seededPermissions, at as publicOperations, b as destinationEventKeys, bt as stagedUploadLifetime, ct as repositoryProviderDefinitions, d as customFieldSchema, dt as resourceProvider, et as permissionScope, f as customFieldValue, ft as roleCategory, g as defaultWorkflow, gt as searchResultTypes, h as defaultOrder, ht as scopes, i as channelRouteAllows, it as projectSchema, jt as workspaceSchema, k as formatEvent, kt as ticketSummarySchema, l as credentialProjectsSchema, lt as repositoryProviders, m as defaultClaimMaxLeaseSeconds, mt as roles, n as attachmentLimit, nt as priorities, p as customFieldValueSchema, pt as roleLabels, r as channelKinds, rt as priorityLabels, s as closedStates, st as remoteKinds, t as ApiError, tt as permissions, u as customFieldFilterTokens, ut as repositorySecretFields, v as deliverySecretFields, vt as signInMethodLabels, w as eventCategories, wt as ticketExportFormat, x as destinationKinds, xt as stateLabels, y as deliverySettingsSchema, yt as snapshotSchema, z as isSeededRole, zt as searchAuthentication } from "./src-BaTNDJB7.mjs";
3
- import { and, asc, count, desc, eq, getTableColumns, gt, ilike, inArray, isNotNull, isNull, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
1
+ import { a as auth_schema_exports, c as oauthConsent, d as user, i as apikey, l as oauthRefreshToken, o as oauthAccessToken, r as account, s as oauthClient, u as session } from "./config-CQVdVhXv.mjs";
2
+ import { $ as parseCustomFieldFilter, A as filterList, At as ticketImportSchema, Bt as instanceUpdateSchema, C as destinationKinds, Ct as stagedUploadLifetime, D as eventLabels, E as eventCategories, Et as states, F as importSecretFields, Ft as workflowOrder, Gt as searchSettingsSchema, I as invitationStatuses, It as workspaceSchema, L as isHexColor, M as hasAdminRole, Mt as ticketSchema, Nt as ticketSummarySchema, O as eventSchema, Ot as ticketExportFormat, Pt as uploadUrlLifetime, Q as operations, Rt as authenticationPolicySchema, S as destinationEventKeys, St as snapshotSchema, Tt as stateLabels, U as mentionScheme, Ut as s3SettingsSchema, V as isSeededRole, W as normalizeRemote, Wt as searchAuthentication, Z as operationPermission, _ as defaultOrder, _t as roles, a as channelRouteAllows, at as priorities, b as deliverySecretFields, bt as seededPermissions, c as claimSchema, ct as publicOperations, d as credentialProjectsSchema, dt as repositoryProviderDefinitions, et as parseOutput, f as customFieldFilterTokens, ft as repositoryProviders, g as defaultClaimMaxLeaseSeconds, gt as roleLabels, h as customFieldValueSchema, ht as roleCategory, i as channelKinds, it as permissions, j as formatEvent, jt as ticketPredicates, k as filterKey, kt as ticketFilter, l as closedStates, m as customFieldValue, mt as resourceProvider, n as attachmentLimit, nt as permissionLabels, ot as priorityLabels, p as customFieldSchema, pt as repositorySecretFields, r as attachmentSchema, rt as permissionScope, st as projectSchema, t as ApiError, tt as parsePublicOutput, ut as remoteKinds, v as defaultWorkflow, vt as scopes, w as difficulties, wt as stagedUploadSchema, x as deliverySettingsSchema, xt as signInMethodLabels, y as deletedAccountName, yt as searchResultTypes, zt as brokerSettingsSchema } from "./src-D70GmZ5R.mjs";
3
+ import { and, arrayContains, arrayOverlaps, asc, count, desc, eq, getTableColumns, gt, inArray, isNotNull, isNull, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
4
4
  import { alias, bigint, boolean, check, customType, foreignKey, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unionAll, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
5
5
  import { z } from "zod";
6
6
  import { decodeJwt } from "jose";
7
7
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "node:crypto";
8
8
  import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
9
+ import { isDeepStrictEqual } from "node:util";
9
10
  import { icons } from "@iconify-json/lucide";
10
11
  import { fromMarkdown } from "mdast-util-from-markdown";
11
12
  import { Expo } from "expo-server-sdk";
@@ -136,6 +137,7 @@ const projects = pgTable("projects", {
136
137
  key: text("key").notNull().unique(),
137
138
  name: text("name").notNull(),
138
139
  description: text("description").notNull().default(""),
140
+ repositories: text("repositories").array().notNull().default([]),
139
141
  nextNumber: integer("next_number").notNull().default(1),
140
142
  position: bigint("position", { mode: "number" }).notNull().default(0),
141
143
  version: integer("version").notNull().default(1),
@@ -304,6 +306,7 @@ const receipts = pgTable("mutation_receipts", {
304
306
  operation: text("operation").notNull(),
305
307
  hash: text("hash").notNull(),
306
308
  result: jsonb("result").notNull(),
309
+ projectId: uuid("project_id"),
307
310
  createdAt: time("created_at")
308
311
  }, (table) => [primaryKey({ columns: [table.actorId, table.requestId] })]);
309
312
  const storageBackends = pgTable("storage_backends", {
@@ -369,6 +372,20 @@ const stagedUploads = pgTable("staged_uploads", {
369
372
  size: integer("size").notNull(),
370
373
  createdAt: time("created_at")
371
374
  }, (table) => [index("staged_uploads_created").on(table.createdAt)]);
375
+ const uploadUrls = pgTable("upload_urls", {
376
+ id: uuid("id").primaryKey(),
377
+ projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
378
+ ticketId: uuid("ticket_id").references(() => tickets.id, { onDelete: "cascade" }),
379
+ actorId: text("actor_id").notNull().references(() => user.id, { onDelete: "cascade" }),
380
+ credential: jsonb("credential"),
381
+ name: text("name").notNull(),
382
+ contentType: text("content_type").notNull(),
383
+ size: integer("size").notNull(),
384
+ tokenHash: text("token_hash").notNull().unique(),
385
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
386
+ usedAt: timestamp("used_at", { withTimezone: true }),
387
+ createdAt: time("created_at")
388
+ }, (table) => [index("upload_urls_expires").on(table.expiresAt)]);
372
389
  const bytea = customType({ dataType: () => "bytea" });
373
390
  const attachmentContent = pgTable("attachment_content", {
374
391
  id: uuid("id").primaryKey().references(() => attachments.id, { onDelete: "cascade" }),
@@ -839,6 +856,7 @@ function createBlobStorage(database, instance, factory = createObjectStore, sche
839
856
  });
840
857
  return database.db.transaction(async (tx) => {
841
858
  await tx.delete(stagedUploads).where(lt(stagedUploads.createdAt, stagedUploadCutoff()));
859
+ await tx.delete(uploadUrls).where(lt(uploadUrls.expiresAt, /* @__PURE__ */ new Date()));
842
860
  const rows = await tx.select({ object: blobObjects }).from(blobObjects).leftJoin(attachments, eq(attachments.objectId, blobObjects.id)).leftJoin(profilePhotos, eq(profilePhotos.objectId, blobObjects.id)).leftJoin(stagedUploads, eq(stagedUploads.id, blobObjects.id)).leftJoin(projects, or(eq(projects.iconObjectId, blobObjects.id), eq(projects.backgroundObjectId, blobObjects.id))).where(and(isNull(attachments.id), isNull(profilePhotos.userId), isNull(stagedUploads.id), isNull(projects.id), lt(blobObjects.createdAt, /* @__PURE__ */ new Date(Date.now() - 864e5)))).limit(20).for("update", {
843
861
  of: blobObjects,
844
862
  skipLocked: true
@@ -1091,14 +1109,23 @@ function parseQuery(query) {
1091
1109
  terms: query
1092
1110
  };
1093
1111
  }
1094
- const querySegments = ({ phrases, terms }) => terms ? [...phrases, terms] : phrases;
1095
- function postgresSearch(query) {
1096
- return and(...querySegments(parseQuery(query)).map((segment) => {
1097
- const value = likePattern(segment);
1098
- return or(ilike(tickets.title, value), ilike(tickets.description, value), ilike(tickets.key, value), ilike(tickets.acceptanceCriteria, value), sql`array_to_string(${tickets.labels}, ' ') ILIKE ${value}`, sql`array_to_string(${tickets.scopes}, ' ') ILIKE ${value}`);
1099
- }));
1100
- }
1101
- const postgresRank = (query) => sql`CASE WHEN ${and(...querySegments(parseQuery(query)).map((segment) => ilike(tickets.title, likePattern(segment))))} THEN 0 ELSE 1 END`;
1112
+ const phrasePattern = (phrase, word) => `(?<!${word})${phrase.replace(/[\\^$.|?*+()[\]{}]/gu, "\\$&").replaceAll(" ", "\\s+")}(?!${word})`;
1113
+ const phraseRegExp = (phrase) => new RegExp(phrasePattern(phrase, "[\\p{L}\\p{N}_]"), "iu");
1114
+ const segmentTests = ({ phrases, terms }) => [...phrases.map((phrase) => {
1115
+ const pattern = phrasePattern(phrase, "[[:alnum:]_]");
1116
+ return (text) => sql`${text} ~* ${pattern}`;
1117
+ }), ...terms ? [(text) => sql`${text} ILIKE ${likePattern(terms)}`] : []];
1118
+ const searchedText = [
1119
+ tickets.title,
1120
+ tickets.description,
1121
+ tickets.key,
1122
+ tickets.acceptanceCriteria,
1123
+ sql`array_to_string(${tickets.labels}, ' ')`,
1124
+ sql`array_to_string(${tickets.scopes}, ' ')`
1125
+ ];
1126
+ const textMatch = (query) => and(...segmentTests(query).map((test) => or(...searchedText.map(test))));
1127
+ const postgresSearch = (query) => textMatch(parseQuery(query));
1128
+ const postgresRank = (query) => sql`CASE WHEN ${and(...segmentTests(parseQuery(query)).map((test) => test(tickets.title)))} THEN 0 ELSE 1 END`;
1102
1129
  const builtinSearch = { where: async (_db, _ids, query) => ({
1103
1130
  where: postgresSearch(query),
1104
1131
  rank: postgresRank(query),
@@ -1164,13 +1191,17 @@ function createTicketSearch(database, instance, factory = createTicketIndex, sch
1164
1191
  if (!state || !clock || state.error || state.configHash !== fingerprint(settings) || state.cursor !== clock.value) return fallback();
1165
1192
  const root = await instance.read();
1166
1193
  try {
1194
+ const parsed = parseQuery(query);
1167
1195
  const hits = await factory({
1168
1196
  provider: "elasticsearch",
1169
1197
  config: settings
1170
- }).find(root.identity, projectIds, parseQuery(query));
1198
+ }).find(root.identity, projectIds, parsed);
1171
1199
  const ids = hits.map((hit) => hit.id), ranks = hits.map((hit) => hit.rank);
1172
1200
  return {
1173
- where: ids.length ? inArray(tickets.id, ids) : sql`false`,
1201
+ where: ids.length ? and(inArray(tickets.id, ids), textMatch({
1202
+ phrases: parsed.phrases,
1203
+ terms: ""
1204
+ })) : sql`false`,
1174
1205
  rank: sql`(${`{${ranks.join(",")}}`}::int[])[array_position(${`{${ids.join(",")}}`}::uuid[], ${tickets.id})]`,
1175
1206
  engine: "index",
1176
1207
  indexing: false
@@ -2413,14 +2444,19 @@ function predicateSQL({ field, operator, values }) {
2413
2444
  const overlap = sql`${tickets.labels} && ${sql.param(values)}::text[]`;
2414
2445
  return operator === "any" ? overlap : sql`NOT (${overlap})`;
2415
2446
  }
2447
+ if (field === "automation") {
2448
+ const ready = sql`(${tickets.automationReady} AND ${tickets.holdReason} IS NULL)`;
2449
+ const match = or(...values.map((value) => value === "ready" ? ready : sql`NOT ${ready}`));
2450
+ return operator === "any" ? match : sql`NOT (${match})`;
2451
+ }
2416
2452
  const column = field === "assigneeId" ? tickets.assigneeId : filterColumns[field];
2417
2453
  const named = field === "assigneeId" ? values.filter((value) => value !== "none") : values, unset = field === "assigneeId" && values.includes("none");
2418
2454
  if (operator === "any") return or(named.length ? inArray(column, named) : void 0, unset ? isNull(column) : void 0);
2419
2455
  return and(named.length ? or(isNull(column), notInArray(column, named)) : void 0, unset ? isNotNull(column) : void 0);
2420
2456
  }
2421
2457
  const predicateWhere = (filter) => and(...ticketPredicates(filter).map(predicateSQL));
2422
- function ticketOrder(input) {
2423
- const direction = input.order ?? defaultOrder[input.sort];
2458
+ function ticketOrder({ sort = "priority", order }) {
2459
+ const direction = order ?? defaultOrder[sort];
2424
2460
  const by = (column) => direction === "asc" ? sql`${column} ASC NULLS LAST` : sql`${column} DESC NULLS LAST`;
2425
2461
  return [{
2426
2462
  priority: by(rank(tickets.priority, priorities)),
@@ -2436,7 +2472,28 @@ function ticketOrder(input) {
2436
2472
  kind: by(tickets.kind),
2437
2473
  assignee: by(sql`(SELECT lower(name) FROM "user" WHERE id = ${tickets.assigneeId})`),
2438
2474
  manual: by(tickets.position)
2439
- }[input.sort], asc(tickets.number)];
2475
+ }[sort], asc(tickets.number)];
2476
+ }
2477
+ const relevanceOrder = (q, relevance) => [
2478
+ sql`CASE WHEN ${ticketMatch(q)} THEN 0 ELSE 1 END`,
2479
+ sql`${relevance} NULLS LAST`,
2480
+ desc(tickets.updatedAt)
2481
+ ];
2482
+ async function listQuery(ctx, projectIds, input) {
2483
+ if (!input.q) return {
2484
+ where: void 0,
2485
+ order: ticketOrder(input)
2486
+ };
2487
+ const where = or(ticketMatch(input.q), postgresSearch(input.q));
2488
+ if (input.sort) return {
2489
+ where,
2490
+ order: ticketOrder(input)
2491
+ };
2492
+ const match = await ctx.search.where(ctx.db, projectIds, input.q);
2493
+ return {
2494
+ where,
2495
+ order: [...relevanceOrder(input.q, match.rank), asc(tickets.number)]
2496
+ };
2440
2497
  }
2441
2498
  async function pageOf(ctx, where, order, input) {
2442
2499
  const rows = await ctx.db.select({
@@ -2527,12 +2584,16 @@ const hitRow = z.object({
2527
2584
  const summarize = (ticket) => ticketSummarySchema.parse(json(ticket));
2528
2585
  const snippetLength = 240;
2529
2586
  function snippet(body, query) {
2530
- const text = body?.replace(/\s+/gu, " ").trim() ?? "", lower = text.toLowerCase(), parsed = parseQuery(query);
2587
+ const text = body?.replace(/\s+/gu, " ").trim() ?? "", lower = text.toLowerCase(), { phrases, terms } = parseQuery(query);
2531
2588
  let at = -1, size = 0;
2532
- for (const needle of [...querySegments(parsed), ...parsed.terms.split(" ")]) {
2533
- const found = needle ? lower.indexOf(needle.toLowerCase()) : -1;
2534
- if (found >= 0 && (at < 0 || found < at)) [at, size] = [found, needle.length];
2589
+ const closest = (found, length) => {
2590
+ if (found >= 0 && (at < 0 || found < at)) [at, size] = [found, length];
2591
+ };
2592
+ for (const phrase of phrases) {
2593
+ const found = phraseRegExp(phrase).exec(text);
2594
+ if (found) closest(found.index, found[0].length);
2535
2595
  }
2596
+ for (const needle of [terms, ...terms.split(" ")]) if (needle) closest(lower.indexOf(needle.toLowerCase()), needle.length);
2536
2597
  if (at < 0) return "";
2537
2598
  if (text.length <= snippetLength) return text;
2538
2599
  const start = Math.max(0, Math.min(at + Math.floor((size - snippetLength) / 2), text.length - snippetLength)), end = start + snippetLength;
@@ -2664,9 +2725,9 @@ const ticketHandlers = {
2664
2725
  async list_tickets(ctx, input) {
2665
2726
  const parent = input.parentId ? await getTicket(ctx, input.parentId) : void 0;
2666
2727
  const related = input.relatedTo ? await getTicket(ctx, input.relatedTo) : void 0;
2667
- const search = input.q ? await ctx.search.where(ctx.db, [project(ctx).id], input.q) : void 0;
2728
+ const search = await listQuery(ctx, [project(ctx).id], input);
2668
2729
  await assertCustomFieldFilters(ctx, [project(ctx).id], input);
2669
- const page = await pageOf(ctx, and(eq(tickets.projectId, project(ctx).id), parent ? eq(tickets.parentId, parent.id) : void 0, related ? or(inArray(tickets.id, ctx.db.select({ id: relations$1.targetId }).from(relations$1).where(eq(relations$1.sourceId, related.id))), inArray(tickets.id, ctx.db.select({ id: relations$1.sourceId }).from(relations$1).where(eq(relations$1.targetId, related.id)))) : void 0, predicateWhere(input), input.archived === "include" ? void 0 : eq(tickets.archived, input.archived === "only"), dueWhere(input.due), input.stateId ? eq(tickets.stateId, input.stateId) : void 0, input.scheduledFrom ? sql`${scheduleEnd} >= ${input.scheduledFrom}::timestamptz` : void 0, input.scheduledTo ? sql`${scheduleStart} < ${input.scheduledTo}::timestamptz` : void 0, search?.where), ticketOrder(input), input);
2730
+ const page = await pageOf(ctx, and(eq(tickets.projectId, project(ctx).id), parent ? eq(tickets.parentId, parent.id) : void 0, related ? or(inArray(tickets.id, ctx.db.select({ id: relations$1.targetId }).from(relations$1).where(eq(relations$1.sourceId, related.id))), inArray(tickets.id, ctx.db.select({ id: relations$1.sourceId }).from(relations$1).where(eq(relations$1.targetId, related.id)))) : void 0, predicateWhere(input), input.archived === "include" ? void 0 : eq(tickets.archived, input.archived === "only"), dueWhere(input.due), input.stateId ? eq(tickets.stateId, input.stateId) : void 0, input.scheduledFrom ? sql`${scheduleEnd} >= ${input.scheduledFrom}::timestamptz` : void 0, input.scheduledTo ? sql`${scheduleStart} < ${input.scheduledTo}::timestamptz` : void 0, search.where), search.order, input);
2670
2731
  return input.view === "summary" ? {
2671
2732
  ...page,
2672
2733
  items: page.items.map(summarize)
@@ -2679,11 +2740,11 @@ const ticketHandlers = {
2679
2740
  total: 0,
2680
2741
  activity: []
2681
2742
  };
2682
- const ids = allowed.map((row) => row.id);
2683
- const where = and(inArray(tickets.projectId, ids), eq(tickets.assigneeId, ctx.actor.id), eq(tickets.archived, false), openTicket, input.q ? (await ctx.search.where(ctx.db, ids, input.q)).where : void 0);
2743
+ const ids = allowed.map((row) => row.id), search = await listQuery(ctx, ids, input);
2744
+ const where = and(inArray(tickets.projectId, ids), eq(tickets.assigneeId, ctx.actor.id), eq(tickets.archived, false), openTicket, search.where);
2684
2745
  const activity = input.activity ? await ctx.db.select().from(events).where(inArray(events.projectId, ids)).orderBy(desc(events.id)).limit(input.activity) : [];
2685
2746
  return {
2686
- ...await pageOf(ctx, where, ticketOrder(input), input),
2747
+ ...await pageOf(ctx, where, search.order, input),
2687
2748
  activity
2688
2749
  };
2689
2750
  },
@@ -2898,7 +2959,7 @@ const ticketHandlers = {
2898
2959
  const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId, "exclude");
2899
2960
  if (!allowed.length) return { items: [] };
2900
2961
  const ids = allowed.map((row) => row.id), match = await ctx.search.where(ctx.db, ids, input.q);
2901
- return { items: (await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, ids), eq(tickets.archived, false), or(ticketMatch(input.q), match.where))).orderBy(sql`CASE WHEN ${ticketMatch(input.q)} THEN 0 ELSE 1 END`, sql`${match.rank} NULLS LAST`, desc(tickets.updatedAt)).limit(input.limit)).flatMap((ticket) => {
2962
+ return { items: (await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, ids), eq(tickets.archived, false), or(ticketMatch(input.q), match.where))).orderBy(...relevanceOrder(input.q, match.rank)).limit(input.limit)).flatMap((ticket) => {
2902
2963
  const owner = allowed.find((row) => row.id === ticket.projectId);
2903
2964
  if (!owner) return [];
2904
2965
  const summary = input.view === "summary", body = `${ticket.description} ${ticket.acceptanceCriteria}`;
@@ -3018,7 +3079,7 @@ const ticketHandlers = {
3018
3079
  },
3019
3080
  async claim_ticket(ctx, input) {
3020
3081
  const ticket = await getTicket(ctx, input.ticketId);
3021
- assertVersion(ticket, input.expectedVersion, "ticket", "claiming");
3082
+ if (input.expectedVersion !== void 0) assertVersion(ticket, input.expectedVersion, "ticket", "claiming");
3022
3083
  const eligible = await readiness(ctx, ticket);
3023
3084
  if (!eligible.ready) fail(409, "not_ready", "The ticket is not ready to claim", eligible);
3024
3085
  const claim = one(await ctx.db.insert(claims).values({
@@ -3040,7 +3101,10 @@ const ticketHandlers = {
3040
3101
  workerId: claim.workerId,
3041
3102
  version: row.version
3042
3103
  });
3043
- return claim;
3104
+ return {
3105
+ ...claim,
3106
+ ticket: row
3107
+ };
3044
3108
  },
3045
3109
  async renew_claim(ctx, input) {
3046
3110
  const ticket = await getTicket(ctx, input.ticketId);
@@ -3411,6 +3475,90 @@ async function addComment(ctx, input) {
3411
3475
  });
3412
3476
  return row;
3413
3477
  }
3478
+ async function attachFile(ctx, ticket, id, bytes, file) {
3479
+ if (intakeOnly(ctx.actor)) {
3480
+ const stored = one(await ctx.db.select({
3481
+ files: sql`count(*) filter (where ${attachments.ticketId} = ${ticket.id})`.mapWith(Number),
3482
+ bytes: sql`coalesce(sum(${attachments.size}), 0)`.mapWith(Number)
3483
+ }).from(attachments).innerJoin(tickets, eq(tickets.id, attachments.ticketId)).where(and(eq(tickets.intakeCredentialId, ctx.actor.ownerId), eq(tickets.holdReason, intakeHoldReason))));
3484
+ if (stored.files >= 10) fail(409, "attachment_limit", `An intake credential can attach at most 10 files to a ticket`);
3485
+ if (stored.bytes + bytes.length > 1e9) fail(409, "attachment_limit", "This intake credential's untriaged tickets hold too many attachment bytes");
3486
+ }
3487
+ await ctx.blobs.put(ctx.db, id, bytes, file.contentType);
3488
+ const row = one(await ctx.db.insert(attachments).values({
3489
+ id,
3490
+ objectId: id,
3491
+ projectId: project(ctx).id,
3492
+ ticketId: ticket.id,
3493
+ actorId: ctx.actor.id,
3494
+ name: file.name,
3495
+ contentType: file.contentType,
3496
+ size: bytes.length
3497
+ }).returning());
3498
+ await publish(ctx, "attachment.created", ticket.id, {
3499
+ attachmentId: row.id,
3500
+ size: row.size
3501
+ });
3502
+ return row;
3503
+ }
3504
+ async function stageFile(ctx, id, bytes, file) {
3505
+ await ctx.blobs.put(ctx.db, id, bytes, file.contentType);
3506
+ const row = one(await ctx.db.insert(stagedUploads).values({
3507
+ id,
3508
+ projectId: project(ctx).id,
3509
+ actorId: ctx.actor.id,
3510
+ name: file.name,
3511
+ contentType: file.contentType,
3512
+ size: bytes.length
3513
+ }).returning());
3514
+ return {
3515
+ ...row,
3516
+ expiresAt: new Date(row.createdAt.getTime() + stagedUploadLifetime)
3517
+ };
3518
+ }
3519
+ const uploadToken = /^kvup_[A-Za-z0-9_-]{43}$/;
3520
+ const hashUploadToken = (token) => createHash("sha256").update(token).digest("hex");
3521
+ const uploadCredential = z.object({
3522
+ ownerId: z.string(),
3523
+ scopes: z.array(z.enum(scopes)),
3524
+ projectIds: z.array(z.string()).optional(),
3525
+ apiKey: z.boolean()
3526
+ }).nullable();
3527
+ function assertUploadUrlOpen(row) {
3528
+ if (row.usedAt) fail(410, "upload_url_unavailable", "This upload URL was already used");
3529
+ if (row.expiresAt.getTime() <= Date.now()) fail(410, "upload_url_unavailable", "This upload URL expired; request a new one");
3530
+ }
3531
+ async function receiveUpload(domain, token, contentLength, read) {
3532
+ const { db } = domain.database, row = uploadToken.test(token) ? (await db.select().from(uploadUrls).where(eq(uploadUrls.tokenHash, hashUploadToken(token))))[0] : void 0;
3533
+ if (!row) return fail(404, "not_found", "This upload URL does not exist");
3534
+ assertUploadUrlOpen(row);
3535
+ const wrongSize = (sent) => fail(400, "invalid_attachment", `This upload URL takes exactly ${row.size} bytes with that Content-Length; the request sent ${sent}`);
3536
+ if (contentLength !== String(row.size)) wrongSize(contentLength ?? "no Content-Length");
3537
+ const reserved = /* @__PURE__ */ new Date();
3538
+ const [held] = await db.update(uploadUrls).set({ usedAt: reserved }).where(and(eq(uploadUrls.id, row.id), isNull(uploadUrls.usedAt), gt(uploadUrls.expiresAt, reserved))).returning({ id: uploadUrls.id });
3539
+ if (!held) fail(410, "upload_url_unavailable", "This upload URL is already receiving an upload");
3540
+ try {
3541
+ const bytes = await read(row.size);
3542
+ if (bytes.length !== row.size) wrongSize(bytes.length > row.size ? "more" : String(bytes.length));
3543
+ const credential = uploadCredential.parse(row.credential);
3544
+ if (credential?.apiKey) {
3545
+ const key = (await db.select().from(apikey).where(eq(apikey.id, credential.ownerId)))[0];
3546
+ if (!key?.enabled || key.expiresAt && key.expiresAt <= /* @__PURE__ */ new Date()) fail(401, "unauthorized", "The API credential that requested this upload URL was revoked");
3547
+ }
3548
+ const actor = await domain.actor(row.actorId, credential ? {
3549
+ ownerId: credential.ownerId,
3550
+ scopes: credential.scopes,
3551
+ ...credential.projectIds ? { projectIds: credential.projectIds } : {}
3552
+ } : void 0);
3553
+ return await domain.transact(async (tx, as) => {
3554
+ const ctx = await as(actor, row.projectId);
3555
+ return row.ticketId ? attachmentSchema.parse(json(await attachFile(ctx, await getTicket(ctx, row.ticketId), row.id, bytes, row))) : stagedUploadSchema.parse(json(await stageFile(ctx, row.id, bytes, row)));
3556
+ });
3557
+ } catch (error) {
3558
+ await db.update(uploadUrls).set({ usedAt: null }).where(and(eq(uploadUrls.id, row.id), eq(uploadUrls.usedAt, reserved)));
3559
+ throw error;
3560
+ }
3561
+ }
3414
3562
  const collaborationHandlers = {
3415
3563
  async list_relations(ctx, input) {
3416
3564
  const ticket = await getTicket(ctx, input.ticketId);
@@ -3520,49 +3668,34 @@ const collaborationHandlers = {
3520
3668
  };
3521
3669
  },
3522
3670
  async upload_attachment(ctx, input) {
3523
- const ticket = await getTicket(ctx, input.ticketId);
3524
- const bytes = decodeBlobData(input.data, attachmentLimit);
3525
- if (intakeOnly(ctx.actor)) {
3526
- const stored = one(await ctx.db.select({
3527
- files: sql`count(*) filter (where ${attachments.ticketId} = ${ticket.id})`.mapWith(Number),
3528
- bytes: sql`coalesce(sum(${attachments.size}), 0)`.mapWith(Number)
3529
- }).from(attachments).innerJoin(tickets, eq(tickets.id, attachments.ticketId)).where(and(eq(tickets.intakeCredentialId, ctx.actor.ownerId), eq(tickets.holdReason, intakeHoldReason))));
3530
- if (stored.files >= 10) fail(409, "attachment_limit", `An intake credential can attach at most 10 files to a ticket`);
3531
- if (stored.bytes + bytes.length > 1e9) fail(409, "attachment_limit", "This intake credential's untriaged tickets hold too many attachment bytes");
3532
- }
3533
- const id = crypto.randomUUID();
3534
- await ctx.blobs.put(ctx.db, id, bytes, input.contentType);
3535
- const row = one(await ctx.db.insert(attachments).values({
3536
- id,
3537
- objectId: id,
3538
- projectId: project(ctx).id,
3539
- ticketId: ticket.id,
3540
- actorId: ctx.actor.id,
3541
- name: input.name,
3542
- contentType: input.contentType,
3543
- size: bytes.length
3544
- }).returning());
3545
- await publish(ctx, "attachment.created", ticket.id, {
3546
- attachmentId: row.id,
3547
- size: row.size
3548
- });
3549
- return row;
3671
+ return attachFile(ctx, await getTicket(ctx, input.ticketId), crypto.randomUUID(), decodeBlobData(input.data, attachmentLimit), input);
3550
3672
  },
3551
3673
  async stage_upload(ctx, input) {
3552
- const bytes = decodeBlobData(input.data, attachmentLimit);
3553
- const id = crypto.randomUUID();
3554
- await ctx.blobs.put(ctx.db, id, bytes, input.contentType);
3555
- const row = one(await ctx.db.insert(stagedUploads).values({
3556
- id,
3674
+ return stageFile(ctx, crypto.randomUUID(), decodeBlobData(input.data, attachmentLimit), input);
3675
+ },
3676
+ async create_upload_url(ctx, input) {
3677
+ const ticket = input.ticketId ? await getTicket(ctx, input.ticketId) : void 0, token = `kvup_${randomBytes(32).toString("base64url")}`, { id, ownerId, scopes: granted, projectIds, machine } = ctx.actor, apiKey = machine && (await ctx.db.select({ id: apikey.id }).from(apikey).where(eq(apikey.id, ownerId))).length > 0;
3678
+ const row = one(await ctx.db.insert(uploadUrls).values({
3679
+ id: crypto.randomUUID(),
3557
3680
  projectId: project(ctx).id,
3558
- actorId: ctx.actor.id,
3681
+ ticketId: ticket?.id ?? null,
3682
+ actorId: id,
3683
+ credential: machine ? {
3684
+ ownerId,
3685
+ scopes: granted,
3686
+ projectIds,
3687
+ apiKey
3688
+ } : null,
3559
3689
  name: input.name,
3560
3690
  contentType: input.contentType,
3561
- size: bytes.length
3691
+ size: input.size,
3692
+ tokenHash: hashUploadToken(token),
3693
+ expiresAt: new Date(Date.now() + uploadUrlLifetime)
3562
3694
  }).returning());
3563
3695
  return {
3564
- ...row,
3565
- expiresAt: new Date(row.createdAt.getTime() + stagedUploadLifetime)
3696
+ uploadId: row.id,
3697
+ url: new URL(`/api/uploads/${token}`, ctx.publicUrl).href,
3698
+ expiresAt: row.expiresAt
3566
3699
  };
3567
3700
  },
3568
3701
  async discard_upload(ctx, input) {
@@ -5326,6 +5459,11 @@ async function takePrefix(ctx, prefix, projectId) {
5326
5459
  projectId: owner
5327
5460
  }).onConflictDoNothing();
5328
5461
  }
5462
+ async function assertRepositoriesFree(ctx, projectId, workspaceId, repositories) {
5463
+ if (!repositories.length) return;
5464
+ const [holder] = await ctx.db.select({ id: projects.id }).from(projects).where(and(eq(projects.workspaceId, workspaceId), isNull(projects.deletedAt), isNull(projects.archivedAt), ne(projects.id, projectId), arrayOverlaps(projects.repositories, repositories))).limit(1);
5465
+ if (holder) fail(409, "duplicate_repository", "Another project in this workspace links this repository");
5466
+ }
5329
5467
  async function storeImage(ctx, image) {
5330
5468
  const objectId = crypto.randomUUID();
5331
5469
  await ctx.blobs.put(ctx.db, objectId, decodeImage(image), image.contentType);
@@ -5441,18 +5579,20 @@ const projectHandlers = {
5441
5579
  const current = project(ctx);
5442
5580
  assertVersion(current, input.expectedVersion, "project");
5443
5581
  const key = input.key ?? current.key;
5444
- const sharing = input.publicReadEnabled !== void 0 && input.publicReadEnabled !== current.publicReadEnabled, details = key !== current.key || input.name !== current.name || input.description !== current.description || !!input.visibility && input.visibility !== current.visibility;
5582
+ const sharing = input.publicReadEnabled !== void 0 && input.publicReadEnabled !== current.publicReadEnabled, details = key !== current.key || input.name !== current.name || input.description !== current.description || !!input.visibility && input.visibility !== current.visibility || !!input.repositories && !isDeepStrictEqual(input.repositories, current.repositories);
5445
5583
  if (sharing) requirePermission(ctx.permissions, "sharing.manage", "project");
5446
5584
  if (details || !sharing) requirePermission(ctx.permissions, "project.manage", "project");
5447
5585
  if (key !== current.key) {
5448
5586
  await (await takePrefix(ctx, key, current.id))(current.id);
5449
5587
  await ctx.db.update(tickets).set({ key: sql`${key}::text || '-' || ${tickets.number}` }).where(eq(tickets.projectId, current.id));
5450
5588
  }
5589
+ if (input.repositories && !current.archivedAt) await assertRepositoriesFree(ctx, current.id, current.workspaceId, input.repositories);
5451
5590
  const row = one(await ctx.db.update(projects).set({
5452
5591
  key,
5453
5592
  name: input.name,
5454
5593
  ...input.visibility ? { visibility: input.visibility } : {},
5455
5594
  ...input.publicReadEnabled === void 0 ? {} : { publicReadEnabled: input.publicReadEnabled },
5595
+ ...input.repositories ? { repositories: input.repositories } : {},
5456
5596
  description: input.description,
5457
5597
  version: current.version + 1,
5458
5598
  updatedAt: /* @__PURE__ */ new Date()
@@ -5870,6 +6010,7 @@ async function assertTransferable(ctx, workspaceId) {
5870
6010
  const current = project(ctx);
5871
6011
  if (workspaceId === current.workspaceId) fail(400, "validation_error", "The project is already in this workspace");
5872
6012
  await requireWorkspace(ctx, current.workspaceId, "project.manage");
6013
+ if (!current.archivedAt) await assertRepositoriesFree(ctx, current.id, workspaceId, current.repositories);
5873
6014
  }
5874
6015
  async function projectPeople(ctx, workspaceId = project(ctx).workspaceId) {
5875
6016
  const current = project(ctx), qualifiedUser = sql`${user}."id"`;
@@ -6032,6 +6173,7 @@ const lifecycleHandlers = {
6032
6173
  assertVersion(current, input.expectedVersion, "project", input.archived ? "archiving" : "restoring");
6033
6174
  if (!!current.archivedAt === input.archived) return current;
6034
6175
  if (input.archived) assertNoBlockers(await claimBlockers(ctx));
6176
+ else await assertRepositoriesFree(ctx, current.id, current.workspaceId, current.repositories);
6035
6177
  const row = await changed(ctx, { archivedAt: input.archived ? /* @__PURE__ */ new Date() : null });
6036
6178
  await publish(ctx, input.archived ? "project.archived" : "project.restored", null, { version: row.version });
6037
6179
  return row;
@@ -7058,6 +7200,16 @@ const publicActor = {
7058
7200
  projectIds: []
7059
7201
  };
7060
7202
  const projectMatch = (identifier) => or(sql`${projects.id}::text = ${identifier}`, eq(projects.key, identifier.toUpperCase()), sql`${projects.id} IN (SELECT pp.project_id FROM ${projectPrefixes} pp WHERE pp.prefix = ${identifier.toUpperCase()})`);
7203
+ const namesRemote = (identifier) => /[/@]/.test(identifier);
7204
+ async function projectByRemote(db, actor, identifier) {
7205
+ const remote = normalizeRemote(identifier);
7206
+ const linked = remote ? await db.select().from(projects).where(and(isNull(projects.deletedAt), arrayContains(projects.repositories, [remote]))) : [];
7207
+ const reachable = [];
7208
+ for (const row of linked) if ((!actor.projectIds || actor.projectIds.includes(row.id)) && await projectAccess(db, actor, row)) reachable.push(row);
7209
+ const live = reachable.filter((row) => !row.archivedAt), named = live.length ? live : reachable;
7210
+ if (named.length > 1) fail(409, "ambiguous_project", "More than one of your projects links this repository", { projects: named.map((row) => row.key) });
7211
+ return one(named);
7212
+ }
7061
7213
  const issuedKey = /^([A-Z][A-Z0-9]{1,9})-([1-9]\d{0,8})$/;
7062
7214
  function ticketMatch(identifier) {
7063
7215
  const key = identifier.toUpperCase(), [, prefix, number] = issuedKey.exec(key) ?? [];
@@ -7073,7 +7225,7 @@ async function context(db, actor, scope, projectId, workspaceId, permission = sc
7073
7225
  ctx.permissions = access.permissions;
7074
7226
  }
7075
7227
  if (!projectId) return ctx;
7076
- const row = one(await db.select().from(projects).where(and(isNull(projects.deletedAt), projectMatch(projectId))).limit(1));
7228
+ const row = namesRemote(projectId) ? await projectByRemote(db, actor, projectId) : one(await db.select().from(projects).where(and(isNull(projects.deletedAt), projectMatch(projectId))).limit(1));
7077
7229
  if (actor.projectIds && !actor.projectIds.includes(row.id)) fail(404, "not_found", "Project was not found");
7078
7230
  const access = await projectAccess(db, actor, row);
7079
7231
  if (!access) fail(404, "not_found", "Project was not found");
@@ -7380,10 +7532,13 @@ function createDomain(database, services = {}) {
7380
7532
  const receipt = requestId ? (await tx.select().from(receipts).where(and(eq(receipts.actorId, actor.id), eq(receipts.requestId, requestId))).limit(1))[0] : void 0;
7381
7533
  if (receipt && (receipt.hash !== hash || receipt.operation !== name)) fail(409, "idempotency_conflict", "This requestId was already used for another write");
7382
7534
  if (receipt && (name === "delete_project" || name === "delete_workspace")) return parseOutput(name, receipt.result);
7383
- const ctx = await context(tx, actor, spec.scope, projectId, workspaceId, permission, name);
7535
+ const ctx = await context(tx, actor, spec.scope, receipt?.projectId ?? projectId, workspaceId, permission, name);
7384
7536
  if (receipt) {
7385
7537
  if ((name === "create_time_entry" || name === "update_time_entry" || name === "delete_time_entry") && "ticketId" in input) await getTicket(ctx, input.ticketId);
7386
- const prior = parseOutput(name, receipt.result);
7538
+ const prior = parseOutput(name, name === "claim_ticket" && "ticketId" in input && !operations.claim_ticket.output.safeParse(receipt.result).success ? json({
7539
+ ...claimSchema.parse(receipt.result),
7540
+ ticket: await getTicket(ctx, input.ticketId)
7541
+ }) : receipt.result);
7387
7542
  if (name === "create_project") await context(tx, actor, "read", projectSchema.parse(prior).id);
7388
7543
  if (name === "create_workspace") await requireWorkspace(ctx, workspaceSchema.parse(prior).id, null);
7389
7544
  if (name === "relocate_ticket") await context(tx, actor, "write", ticketSchema.parse(prior).projectId);
@@ -7396,7 +7551,8 @@ function createDomain(database, services = {}) {
7396
7551
  requestId,
7397
7552
  hash,
7398
7553
  operation: name,
7399
- result
7554
+ result,
7555
+ projectId: ctx.project?.id ?? null
7400
7556
  });
7401
7557
  return result;
7402
7558
  });
@@ -7421,7 +7577,7 @@ function createDomain(database, services = {}) {
7421
7577
  }
7422
7578
  async function readEvents(identity, cursor, projectId) {
7423
7579
  if (!hasScope(identity, "read")) fail(403, "insufficient_scope", "Read permission is required");
7424
- const allowed = await accessibleProjects(identity), named = projectId ? await database.db.select({ id: projects.id }).from(projects).where(projectMatch(projectId)) : [];
7580
+ const allowed = await accessibleProjects(identity), named = !projectId ? [] : namesRemote(projectId) ? [await projectByRemote(database.db, identity, projectId)] : await database.db.select({ id: projects.id }).from(projects).where(projectMatch(projectId));
7425
7581
  const ids = allowed.filter((row) => !projectId || named.some((match) => match.id === row.id)).map((row) => row.id);
7426
7582
  if (projectId && !ids.length) fail(404, "not_found", "Project was not found");
7427
7583
  const clock = one(await database.db.select().from(eventClock).where(eq(eventClock.id, 1)));
@@ -8102,4 +8258,4 @@ function createAuth(database, config) {
8102
8258
  };
8103
8259
  }
8104
8260
  //#endregion
8105
- export { stagedUploads as $, requireSession as A, attachmentContent as B, defaultSenders as C, createAccess as D, createHttpSender as E, decodeImage as F, events as G, blobObjects as H, imageResponse as I, notificationPreferences as J, instanceSettings as K, stagedUploadCutoff as L, createTicketSearch as M, createTicketIndex as N, createWriteLimiter as O, createBlobStorage as P, pushDevices as Q, createObjectStore as R, receiveRepositoryWebhook as S, DeliveryError as T, deliveryCheckpoints as U, attachments as V, eventClock as W, profilePhotos as X, notifications as Y, projects as Z, one as _, deliverySecrets as a, matchesOIDCBinding as at, requireRow as b, ownerBlocker as c, createDomain as d, storageBackends as et, errorEnvelope as f, notify as g, json as h, createInstance as i, workspaces as it, readCalendarFeed as j, requireInstanceAdmin as k, appendEvents as l, intakeOnly as m, emailCodesAvailable as n, tickets as nt, otherActiveAdministrator as o, fail as p, members as q, signInProviders as r, workspaceMembers as rt, ownedWorkspaces as s, createAuth as t, subscriptions as tt, assertVersion as u, reachable as v, deliverBatch as w, processImports as x, recipientColumns as y, createPollingJob as z };
8261
+ export { pushDevices as $, requireInstanceAdmin as A, createPollingJob as B, defaultSenders as C, receiveUpload as D, createHttpSender as E, createBlobStorage as F, eventClock as G, attachments as H, decodeImage as I, members as J, events as K, imageResponse as L, readCalendarFeed as M, createTicketSearch as N, createAccess as O, createTicketIndex as P, projects as Q, stagedUploadCutoff as R, receiveRepositoryWebhook as S, DeliveryError as T, blobObjects as U, attachmentContent as V, deliveryCheckpoints as W, notifications as X, notificationPreferences as Y, profilePhotos as Z, one as _, deliverySecrets as a, workspaces as at, requireRow as b, ownerBlocker as c, createDomain as d, stagedUploads as et, errorEnvelope as f, notify as g, json as h, createInstance as i, workspaceMembers as it, requireSession as j, createWriteLimiter as k, appendEvents as l, intakeOnly as m, emailCodesAvailable as n, subscriptions as nt, otherActiveAdministrator as o, matchesOIDCBinding as ot, fail as p, instanceSettings as q, signInProviders as r, tickets as rt, ownedWorkspaces as s, createAuth as t, storageBackends as tt, assertVersion as u, reachable as v, deliverBatch as w, processImports as x, recipientColumns as y, createObjectStore as z };
@@ -1,4 +1,4 @@
1
- import { C as errorSchema, Ct as streamErrorSchema, E as eventSchema, G as notificationReadInput, H as notificationIdsInput, K as notificationsSchema, Q as parsePublicOutput, U as notificationPreferencesInput, W as notificationPreferencesSchema, Y as operations, Z as parseOutput, a as checkpointSchema, at as publicOperations, q as okSchema, t as ApiError, yt as snapshotSchema } from "./src-BaTNDJB7.mjs";
1
+ import { Dt as streamErrorSchema, G as notificationIdsInput, J as notificationReadInput, K as notificationPreferencesInput, O as eventSchema, Q as operations, St as snapshotSchema, T as errorSchema, X as okSchema, Y as notificationsSchema, ct as publicOperations, et as parseOutput, o as checkpointSchema, q as notificationPreferencesSchema, t as ApiError, tt as parsePublicOutput } from "./src-D70GmZ5R.mjs";
2
2
  import { z } from "zod";
3
3
  import { parseArgs } from "node:util";
4
4
  import { constants } from "node:fs";
@@ -653,7 +653,7 @@ profile list | profile remove <name> --yes
653
653
  --input <json|-> Operation input as a JSON object; - reads stdin
654
654
  --title <text> Ticket title
655
655
  --body <markdown> Comment body
656
- --expected-version <n> Required version for update, delete, acquire and rescheduling recovery
656
+ --expected-version <n> Required version for update, delete and rescheduling recovery; optional for acquire
657
657
  --changes <json> Ticket update changes as a JSON object
658
658
  --request-id <uuid> Reuse for a deliberate retry across invocations
659
659
  --json Compact JSON output (default: indented JSON)
@@ -1,4 +1,4 @@
1
- import { Bt as searchSettingsSchema, Pt as brokerSettingsSchema, Rt as s3SettingsSchema, m as defaultClaimMaxLeaseSeconds, o as claimLeaseSecondsSchema } from "./src-BaTNDJB7.mjs";
1
+ import { Gt as searchSettingsSchema, Ut as s3SettingsSchema, g as defaultClaimMaxLeaseSeconds, s as claimLeaseSecondsSchema, zt as brokerSettingsSchema } from "./src-D70GmZ5R.mjs";
2
2
  import pg from "pg";
3
3
  import { drizzle } from "drizzle-orm/node-postgres";
4
4
  import { relations } from "drizzle-orm";