@maudecode/kanvil 0.5.0 → 0.6.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 (133) hide show
  1. package/apps/server/dist/{auth-Cs_whzgk.mjs → auth-CFjetlB4.mjs} +63 -15
  2. package/apps/server/dist/client.mjs +10 -9
  3. package/apps/server/dist/{config-DKuNQY5b.mjs → config-BQw6mPzE.mjs} +1 -1
  4. package/apps/server/dist/index.mjs +10 -5
  5. package/apps/server/dist/migrate.mjs +1 -1
  6. package/apps/server/dist/recover.mjs +3 -3
  7. package/apps/server/dist/{src-DptOUNRd.mjs → src-BaTNDJB7.mjs} +93 -5
  8. package/apps/server/drizzle/0039_intake_credential.sql +2 -0
  9. package/apps/server/drizzle/meta/0039_snapshot.json +7020 -0
  10. package/apps/server/drizzle/meta/_journal.json +7 -0
  11. package/apps/web/dist/assets/{CompositeItem-CXqewtEL.js → CompositeItem-Dotj8JZL.js} +1 -1
  12. package/apps/web/dist/assets/{Trans-B1rRlWca.js → Trans-CE4EAAY1.js} +1 -1
  13. package/apps/web/dist/assets/admin-settings-CO7nnD9T.js +1 -0
  14. package/apps/web/dist/assets/{archive-restore-76sStk8K.js → archive-restore-BMtgjFUu.js} +1 -1
  15. package/apps/web/dist/assets/az-AZ-R8_HFWJr.js +1 -0
  16. package/apps/web/dist/assets/{az-B7OgHoaE.js → az-B-DxMc__.js} +1 -1
  17. package/apps/web/dist/assets/configuration-Bnt72HRu.js +1 -0
  18. package/apps/web/dist/assets/{corner-left-up-PD-73Nae.js → corner-left-up-O4OsTW-L.js} +1 -1
  19. package/apps/web/dist/assets/{de-B-bv38Q0.js → de-Btbkq-aX.js} +1 -1
  20. package/apps/web/dist/assets/de-DE-BHgMj19Y.js +1 -0
  21. package/apps/web/dist/assets/default-DqsN66dq.js +56 -0
  22. package/apps/web/dist/assets/delivery-settings-Cji0N0m0.js +1 -0
  23. package/apps/web/dist/assets/{el-BQHQHQgX.js → el-DBVEX16k.js} +1 -1
  24. package/apps/web/dist/assets/el-GR-CQFGM5Sr.js +1 -0
  25. package/apps/web/dist/assets/{ellipsis-GF1HwTDb.js → ellipsis-CvEPDi9V.js} +1 -1
  26. package/apps/web/dist/assets/{es-f5Xr8ldr.js → es-CMJoxZBk.js} +1 -1
  27. package/apps/web/dist/assets/es-ES-DZkcRVA4.js +1 -0
  28. package/apps/web/dist/assets/{forms-BlEL8C_P.js → forms-DbfnnPg_.js} +1 -1
  29. package/apps/web/dist/assets/fr-FR-CRPAezXU.js +1 -0
  30. package/apps/web/dist/assets/{fr-DSR0htnl.js → fr-OyEmYoRn.js} +1 -1
  31. package/apps/web/dist/assets/{hi-BaAJEWuW.js → hi-CIgXSseB.js} +1 -1
  32. package/apps/web/dist/assets/hi-IN-2YFE4UQk.js +1 -0
  33. package/apps/web/dist/assets/{highlight-DXT2xgt8.js → highlight-B4K3yEEh.js} +1 -1
  34. package/apps/web/dist/assets/id-ID-B-J0_QIW.js +1 -0
  35. package/apps/web/dist/assets/{id-kxogWcip.js → id-WV2oSAPB.js} +1 -1
  36. package/apps/web/dist/assets/index-BAnwOrh1.css +2 -0
  37. package/apps/web/dist/assets/index-nltuyQtq.js +17 -0
  38. package/apps/web/dist/assets/invitations-BTB8f2Xl.js +1 -0
  39. package/apps/web/dist/assets/{it-C_qk40U6.js → it-BWSrRqus.js} +1 -1
  40. package/apps/web/dist/assets/it-IT-CJ0HYggO.js +1 -0
  41. package/apps/web/dist/assets/item-HmFuRZet.js +1 -0
  42. package/apps/web/dist/assets/{ja-Dn5h64V9.js → ja-BtOHWvUJ.js} +1 -1
  43. package/apps/web/dist/assets/ja-JP-DDQS7D7b.js +1 -0
  44. package/apps/web/dist/assets/{ko-CjaoEPuG.js → ko-DAryK0HN.js} +1 -1
  45. package/apps/web/dist/assets/ko-KR-CkpoewEa.js +1 -0
  46. package/apps/web/dist/assets/label-settings-ssfEX2uR.js +1 -0
  47. package/apps/web/dist/assets/{markdown-blocks-BQLO-H_7.js → markdown-blocks-B1kO6iku.js} +1 -1
  48. package/apps/web/dist/assets/member-directory-BA4ZT1qU.js +1 -0
  49. package/apps/web/dist/assets/{mk-jLeGD50T.js → mk-DBbiGUk5.js} +1 -1
  50. package/apps/web/dist/assets/mk-MK-C0RhS6XR.js +1 -0
  51. package/apps/web/dist/assets/{nl-AVtC5ygT.js → nl-D4V-8WdF.js} +1 -1
  52. package/apps/web/dist/assets/nl-NL-BSKqlzcL.js +1 -0
  53. package/apps/web/dist/assets/{pl-Dup1h_Y1.js → pl-C--QrxO0.js} +1 -1
  54. package/apps/web/dist/assets/pl-PL-CHz38SiU.js +1 -0
  55. package/apps/web/dist/assets/project-lifecycle-COZb9FqM.js +1 -0
  56. package/apps/web/dist/assets/project-settings-DwmTm9KA.js +1 -0
  57. package/apps/web/dist/assets/pt-BR-BotWHRiO.js +1 -0
  58. package/apps/web/dist/assets/{pt-BR-Cm04EWM-.js → pt-BR-DvFVKpmO.js} +1 -1
  59. package/apps/web/dist/assets/{public-project-BivDjU_a.js → public-project-B3hEkeCF.js} +1 -1
  60. package/apps/web/dist/assets/{rotate-ccw-CSaKhZYt.js → rotate-ccw-CB36Zu3L.js} +1 -1
  61. package/apps/web/dist/assets/ru-RU-DtOwNna6.js +1 -0
  62. package/apps/web/dist/assets/{ru-IeycXsyE.js → ru-UmbPUOka.js} +1 -1
  63. package/apps/web/dist/assets/search-Cvetr1OK.js +85 -0
  64. package/apps/web/dist/assets/{send-BLMZ8gQ0.js → send-CJDct4Oj.js} +1 -1
  65. package/apps/web/dist/assets/settings-3IXm9lJ-.js +1 -0
  66. package/apps/web/dist/assets/{sliders-horizontal-Db6kRvuw.js → sliders-horizontal-dmqWngxy.js} +1 -1
  67. package/apps/web/dist/assets/switch-BtScTpP9.js +1 -0
  68. package/apps/web/dist/assets/{table-UJp5S9yy.js → table-byFokzo-.js} +1 -1
  69. package/apps/web/dist/assets/ticket-detail-C3YA3Hzc.js +2 -0
  70. package/apps/web/dist/assets/ticket-editor-CZnZQMG1.js +2 -0
  71. package/apps/web/dist/assets/ticket-list-BXUorfmj.js +6 -0
  72. package/apps/web/dist/assets/ticket-list-controls-BxGKCZMo.js +1 -0
  73. package/apps/web/dist/assets/{tr-DtT_dKUK.js → tr-CNpgQ4ka.js} +1 -1
  74. package/apps/web/dist/assets/tr-TR-CdG2alU2.js +1 -0
  75. package/apps/web/dist/assets/{uk-CsbeHSS6.js → uk-MQoTXdpk.js} +1 -1
  76. package/apps/web/dist/assets/uk-UA-Ceu5gorw.js +1 -0
  77. package/apps/web/dist/assets/{upload-DUEg04Nj.js → upload-D1x_ib3B.js} +1 -1
  78. package/apps/web/dist/assets/useQueries-BnlNCoy4.js +1 -0
  79. package/apps/web/dist/assets/{vi-rkf1BOLx.js → vi-CEw4gTWc.js} +1 -1
  80. package/apps/web/dist/assets/vi-VN-CxshgV4W.js +1 -0
  81. package/apps/web/dist/assets/visual-editor-CpdMRn3e.js +208 -0
  82. package/apps/web/dist/assets/workspace-settings-BickBtZx.js +1 -0
  83. package/apps/web/dist/assets/{zh-CN-Xo-QId5w.js → zh-CN-ChzoXkfr.js} +1 -1
  84. package/apps/web/dist/assets/zh-CN-D7Yrjhd3.js +1 -0
  85. package/apps/web/dist/assets/zh-TW-DyF38kqx.js +1 -0
  86. package/apps/web/dist/assets/{zh-TW-DEnTqjbn.js → zh-TW-bWBKARrK.js} +1 -1
  87. package/apps/web/dist/index.html +9 -9
  88. package/docs/api.md +5 -3
  89. package/docs/cli.md +1 -1
  90. package/docs/mobile.md +67 -4
  91. package/docs/releasing.md +6 -4
  92. package/package.json +1 -1
  93. package/apps/web/dist/assets/admin-settings-DE3MfBFE.js +0 -1
  94. package/apps/web/dist/assets/az-AZ-bFxpMMZN.js +0 -1
  95. package/apps/web/dist/assets/de-DE-BeXI30Ll.js +0 -1
  96. package/apps/web/dist/assets/default-bQFGTwIp.js +0 -56
  97. package/apps/web/dist/assets/delivery-settings-BvMeOsCS.js +0 -1
  98. package/apps/web/dist/assets/el-GR-SgRHlWhY.js +0 -1
  99. package/apps/web/dist/assets/es-ES-BF1J5vap.js +0 -1
  100. package/apps/web/dist/assets/fr-FR-D_sldwf6.js +0 -1
  101. package/apps/web/dist/assets/hi-IN-CfOw4TWf.js +0 -1
  102. package/apps/web/dist/assets/id-ID-CLKtHO7Q.js +0 -1
  103. package/apps/web/dist/assets/index-CYVrs22O.js +0 -17
  104. package/apps/web/dist/assets/index-DyQNFS69.css +0 -2
  105. package/apps/web/dist/assets/invitations-BPik3J_G.js +0 -1
  106. package/apps/web/dist/assets/it-IT-eVZNW5qX.js +0 -1
  107. package/apps/web/dist/assets/item-TCHuZNK6.js +0 -1
  108. package/apps/web/dist/assets/ja-JP-D96njn-P.js +0 -1
  109. package/apps/web/dist/assets/ko-KR-Bash_ieA.js +0 -1
  110. package/apps/web/dist/assets/label-settings-DZTorNDm.js +0 -1
  111. package/apps/web/dist/assets/member-directory-Dx_jQYfI.js +0 -1
  112. package/apps/web/dist/assets/mk-MK-BWQWGIRx.js +0 -1
  113. package/apps/web/dist/assets/nl-NL-D1tvo0Gd.js +0 -1
  114. package/apps/web/dist/assets/pl-PL-DNgGUEcQ.js +0 -1
  115. package/apps/web/dist/assets/project-lifecycle-n1ZZbYfu.js +0 -1
  116. package/apps/web/dist/assets/project-settings-B_Ih5kiz.js +0 -1
  117. package/apps/web/dist/assets/pt-BR-CZsUZQ6P.js +0 -1
  118. package/apps/web/dist/assets/ru-RU-4ew92YE_.js +0 -1
  119. package/apps/web/dist/assets/search-Ble6WSEU.js +0 -85
  120. package/apps/web/dist/assets/settings-CH48qQK2.js +0 -1
  121. package/apps/web/dist/assets/switch-BeX-0Fwq.js +0 -1
  122. package/apps/web/dist/assets/ticket-detail-C24opfNW.js +0 -2
  123. package/apps/web/dist/assets/ticket-editor--d_bG9C1.js +0 -2
  124. package/apps/web/dist/assets/ticket-list-DRbZWYHf.js +0 -5
  125. package/apps/web/dist/assets/ticket-list-controls-fYBottFC.js +0 -1
  126. package/apps/web/dist/assets/tr-TR-CM381XCw.js +0 -1
  127. package/apps/web/dist/assets/uk-UA-AeOiWCVo.js +0 -1
  128. package/apps/web/dist/assets/useQueries-5lW2C5Sb.js +0 -1
  129. package/apps/web/dist/assets/vi-VN-td08xXv9.js +0 -1
  130. package/apps/web/dist/assets/visual-editor-DkFLjouD.js +0 -208
  131. package/apps/web/dist/assets/workspace-settings-Bcsp-VoJ.js +0 -1
  132. package/apps/web/dist/assets/zh-CN-D3dhEYLS.js +0 -1
  133. package/apps/web/dist/assets/zh-TW-CfhOEFo8.js +0 -1
@@ -1,5 +1,5 @@
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-DKuNQY5b.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-DptOUNRd.mjs";
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
3
  import { and, asc, count, desc, eq, getTableColumns, gt, ilike, 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";
@@ -218,6 +218,7 @@ const tickets = pgTable("tickets", {
218
218
  automationReady: boolean("automation_ready").notNull().default(false),
219
219
  claimRequired: boolean("claim_required").notNull().default(false),
220
220
  holdReason: text("hold_reason"),
221
+ intakeCredentialId: text("intake_credential_id"),
221
222
  startAt: timestamp("start_at", { withTimezone: true }),
222
223
  dueAt: timestamp("due_at", { withTimezone: true }),
223
224
  archived: boolean("archived").notNull().default(false),
@@ -233,6 +234,7 @@ const tickets = pgTable("tickets", {
233
234
  index("ticket_parent").on(table.parentId),
234
235
  index("ticket_project_position").on(table.projectId, table.position),
235
236
  index("ticket_state").on(table.stateId),
237
+ index("ticket_intake_credential").on(table.intakeCredentialId).where(sql`${table.intakeCredentialId} IS NOT NULL`),
236
238
  foreignKey({
237
239
  name: "ticket_workflow_state",
238
240
  columns: [
@@ -711,8 +713,7 @@ const createObjectStore = (configuration) => {
711
713
  if (!create) throw new Error("Unsupported object storage provider");
712
714
  return create(configuration.config);
713
715
  };
714
- //#endregion
715
- //#region src/blob-storage.ts
716
+ const intakeAttachmentLifetime = 2592e6;
716
717
  const stagedUploadCutoff = () => new Date(Date.now() - stagedUploadLifetime);
717
718
  const digest$1 = (bytes) => createHash("sha256").update(bytes).digest("hex");
718
719
  function decodeBlobData(value, limit) {
@@ -811,6 +812,31 @@ function createBlobStorage(database, instance, factory = createObjectStore, sche
811
812
  });
812
813
  }
813
814
  async function collectBatch() {
815
+ await database.db.transaction(async (tx) => {
816
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(7361080)`);
817
+ const expired = await tx.select({
818
+ attachment: attachments,
819
+ key: tickets.key,
820
+ title: tickets.title
821
+ }).from(attachments).innerJoin(tickets, eq(tickets.id, attachments.ticketId)).where(and(isNotNull(tickets.intakeCredentialId), eq(tickets.holdReason, intakeHoldReason), lt(attachments.createdAt, /* @__PURE__ */ new Date(Date.now() - intakeAttachmentLifetime)))).orderBy(asc(attachments.createdAt), asc(attachments.id)).limit(100);
822
+ if (!expired.length) return;
823
+ await tx.delete(attachments).where(inArray(attachments.id, expired.map(({ attachment }) => attachment.id)));
824
+ await appendEvents(tx, expired.map(({ attachment, key, title }) => ({
825
+ projectId: attachment.projectId,
826
+ ticketId: attachment.ticketId,
827
+ actorId: null,
828
+ type: "attachment.deleted",
829
+ data: {
830
+ attachmentId: attachment.id,
831
+ key,
832
+ title,
833
+ source: {
834
+ kind: "system",
835
+ id: "blob-cleanup"
836
+ }
837
+ }
838
+ })));
839
+ });
814
840
  return database.db.transaction(async (tx) => {
815
841
  await tx.delete(stagedUploads).where(lt(stagedUploads.createdAt, stagedUploadCutoff()));
816
842
  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", {
@@ -2453,7 +2479,8 @@ async function createTicketRow(ctx, input) {
2453
2479
  customFields: intakeOnly(ctx.actor) ? {} : applyCustomFieldValues(await projectCustomFields(ctx), null, customFields),
2454
2480
  ...await allocateTicket(ctx),
2455
2481
  id,
2456
- createdBy: ctx.actor.id
2482
+ createdBy: ctx.actor.id,
2483
+ intakeCredentialId: intakeOnly(ctx.actor) ? ctx.actor.ownerId : null
2457
2484
  }).returning());
2458
2485
  await subscribe(ctx, row.projectId, row.id, [ctx.actor.id, row.assigneeId]);
2459
2486
  return {
@@ -3036,11 +3063,17 @@ const ticketHandlers = {
3036
3063
  },
3037
3064
  async recover_claim(ctx, input) {
3038
3065
  const ticket = await getTicket(ctx, input.ticketId);
3039
- const current = one(await ctx.db.select().from(claims).where(and(eq(claims.id, input.claimId), eq(claims.ticketId, ticket.id))));
3040
- if (input.cancelActive && !can(ctx, "project.manage")) fail(403, "forbidden", "Cancelling an active claim requires project management access and admin scope");
3041
- if (current.expiresAt > /* @__PURE__ */ new Date() && !input.cancelActive) fail(409, "claim_not_expired", "The current claim has not expired");
3042
- if (input.reschedule && input.expectedVersion !== void 0) {
3043
- assertVersion(ticket, input.expectedVersion, "ticket", "rescheduling recovered work");
3066
+ const current = input.claimId === void 0 ? void 0 : one(await ctx.db.select().from(claims).where(and(eq(claims.id, input.claimId), eq(claims.ticketId, ticket.id))));
3067
+ if (!current) {
3068
+ const held = (await ctx.db.select({ id: claims.id }).from(claims).where(eq(claims.ticketId, ticket.id)).limit(1))[0];
3069
+ if (held) fail(409, "claim_conflict", "This ticket has a claim; recover it by its claim ID", { claimId: held.id });
3070
+ if (!ticket.claimRequired) fail(409, "claim_not_required", "This ticket does not require ownership to recover");
3071
+ } else {
3072
+ if (input.cancelActive && !can(ctx, "project.manage")) fail(403, "forbidden", "Cancelling an active claim requires project management access and admin scope");
3073
+ if (current.expiresAt > /* @__PURE__ */ new Date() && !input.cancelActive) fail(409, "claim_not_expired", "The current claim has not expired");
3074
+ }
3075
+ if (input.expectedVersion !== void 0 && (input.reschedule || !current)) assertVersion(ticket, input.expectedVersion, "ticket", "recovering ownership");
3076
+ if (input.reschedule) {
3044
3077
  const row = one(await ctx.db.update(tickets).set({
3045
3078
  ...await resolveState(ctx, { status: "todo" }, ticket),
3046
3079
  version: ticket.version + 1,
@@ -3061,12 +3094,12 @@ const ticketHandlers = {
3061
3094
  changedFields: ["claimRequired"]
3062
3095
  });
3063
3096
  }
3064
- await ctx.db.delete(claims).where(eq(claims.id, current.id));
3097
+ if (current) await ctx.db.delete(claims).where(eq(claims.id, current.id));
3065
3098
  await publish(ctx, "claim.recovered", ticket.id, {
3066
- claimId: current.id,
3099
+ claimId: current?.id ?? null,
3067
3100
  reason: input.reason,
3068
3101
  confirmedStopped: true,
3069
- ...input.cancelActive && { cancelActive: true }
3102
+ ...current && input.cancelActive && { cancelActive: true }
3070
3103
  });
3071
3104
  return { ok: true };
3072
3105
  }
@@ -3489,6 +3522,14 @@ const collaborationHandlers = {
3489
3522
  async upload_attachment(ctx, input) {
3490
3523
  const ticket = await getTicket(ctx, input.ticketId);
3491
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
+ }
3492
3533
  const id = crypto.randomUUID();
3493
3534
  await ctx.blobs.put(ctx.db, id, bytes, input.contentType);
3494
3535
  const row = one(await ctx.db.insert(attachments).values({
@@ -6963,7 +7004,12 @@ const complete = (status) => closedStates.some((closed) => closed === status);
6963
7004
  const intakeHoldReason = "Untrusted intake: human triage required";
6964
7005
  const intakeQuarantined = (ticket) => ticket.holdReason === intakeHoldReason;
6965
7006
  const intakeOnly = (actor) => actor.scopes.length > 0 && actor.scopes.every((scope) => scope === "intake");
6966
- const hasScope = (actor, scope, operation) => intakeOnly(actor) ? operation === "create_ticket" : actor.scopes.includes("admin") || actor.scopes.includes(scope) || scope === "read" && actor.scopes.some((value) => value !== "intake");
7007
+ const intakeOperations = /* @__PURE__ */ new Set([
7008
+ "create_ticket",
7009
+ "upload_attachment",
7010
+ "list_attachments"
7011
+ ]);
7012
+ const hasScope = (actor, scope, operation) => intakeOnly(actor) ? !!operation && intakeOperations.has(operation) : actor.scopes.includes("admin") || actor.scopes.includes(scope) || scope === "read" && actor.scopes.some((value) => value !== "intake");
6967
7013
  function requireScope(actor, scope, operation) {
6968
7014
  if (!hasScope(actor, scope, operation)) fail(403, "insufficient_scope", `This credential requires ${scope} permission`);
6969
7015
  }
@@ -7040,7 +7086,9 @@ async function context(db, actor, scope, projectId, workspaceId, permission = sc
7040
7086
  };
7041
7087
  }
7042
7088
  async function getTicket(ctx, identifier) {
7043
- return one(await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, project(ctx).id), ticketMatch(identifier))).limit(1));
7089
+ const ticket = one(await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, project(ctx).id), ticketMatch(identifier))).limit(1));
7090
+ if (intakeOnly(ctx.actor) && !(ticket.intakeCredentialId === ctx.actor.ownerId && intakeQuarantined(ticket))) fail(404, "not_found", "The requested record was not found");
7091
+ return ticket;
7044
7092
  }
7045
7093
  const insertBatch = 1e3;
7046
7094
  async function insertAll(rows, insert) {
@@ -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-DptOUNRd.mjs";
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";
2
2
  import { z } from "zod";
3
3
  import { parseArgs } from "node:util";
4
4
  import { constants } from "node:fs";
@@ -403,7 +403,7 @@ function createClient(options) {
403
403
  return request(url, init);
404
404
  }
405
405
  };
406
- const send = async (spec, input, base) => {
406
+ const send = async (spec, input, base, signal) => {
407
407
  const values = { ...spec.input.parse(input) };
408
408
  const path = spec.path.replace(/\{(\w+)\}/g, (_, key) => {
409
409
  const value = values[key];
@@ -416,7 +416,8 @@ function createClient(options) {
416
416
  const init = {
417
417
  method: spec.method,
418
418
  credentials: options.credentials ?? "include",
419
- headers
419
+ headers,
420
+ ...signal ? { signal } : {}
420
421
  };
421
422
  if (spec.method === "GET") {
422
423
  for (const [key, value] of Object.entries(values)) for (const item of Array.isArray(value) ? value : [value]) if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") url.searchParams.append(key, String(item));
@@ -437,11 +438,11 @@ function createClient(options) {
437
438
  credentials: options.credentials ?? "include"
438
439
  })));
439
440
  },
440
- async call(name, input) {
441
- return parseOutput(name, await send(operations[name], input, "/api/v1"));
441
+ async call(name, input, signal) {
442
+ return parseOutput(name, await send(operations[name], input, "/api/v1", signal));
442
443
  },
443
- async callPublic(name, input) {
444
- return parsePublicOutput(name, await send(publicOperations[name], input, "/api/public"));
444
+ async callPublic(name, input, signal) {
445
+ return parsePublicOutput(name, await send(publicOperations[name], input, "/api/public", signal));
445
446
  },
446
447
  async subscribe(subscription) {
447
448
  let cursor = subscription.cursor, backoff = 1e3;
@@ -599,7 +600,7 @@ function createClient(options) {
599
600
  name,
600
601
  input
601
602
  ],
602
- queryFn: () => api.call(name, input)
603
+ queryFn: ({ signal } = {}) => api.call(name, input, signal)
603
604
  };
604
605
  },
605
606
  publicQuery(name, input) {
@@ -610,7 +611,7 @@ function createClient(options) {
610
611
  name,
611
612
  input
612
613
  ],
613
- queryFn: () => api.callPublic(name, input)
614
+ queryFn: ({ signal } = {}) => api.callPublic(name, input, signal)
614
615
  };
615
616
  },
616
617
  action(name) {
@@ -1,4 +1,4 @@
1
- import { Bt as searchSettingsSchema, Pt as brokerSettingsSchema, Rt as s3SettingsSchema, m as defaultClaimMaxLeaseSeconds, o as claimLeaseSecondsSchema } from "./src-DptOUNRd.mjs";
1
+ import { Bt as searchSettingsSchema, Pt as brokerSettingsSchema, Rt as s3SettingsSchema, m as defaultClaimMaxLeaseSeconds, o as claimLeaseSecondsSchema } from "./src-BaTNDJB7.mjs";
2
2
  import pg from "pg";
3
3
  import { drizzle } from "drizzle-orm/node-postgres";
4
4
  import { relations } from "drizzle-orm";
@@ -1,7 +1,7 @@
1
- import { a as auth_schema_exports, d as user$1, i as apikey, n as createDatabase, r as account, t as readConfig, u as session } from "./config-DKuNQY5b.mjs";
2
- import { A as hasAdminRole, B as memberSchema, Bt as searchSettingsSchema, C as errorSchema, Ft as instanceUpdateSchema, G as notificationReadInput, H as notificationIdsInput, I as isOperation, It as photoInputSchema, L as isPresetColor, Lt as photoResultSchema, M as importProviderDefinitions, Mt as adminInstanceSchema, Pt as brokerSettingsSchema, R as isPublicOperation, Rt as s3SettingsSchema, U as notificationPreferencesInput, W as notificationPreferencesSchema, Y as operations, at as publicOperations, c as credentialInput, g as defaultWorkflow, ht as scopes, j as importLimits, mt as roles, ot as pushDeviceInput, q as okSchema, s as closedStates, t as ApiError, wt as ticketExportFormat, xt as stateLabels } from "./src-DptOUNRd.mjs";
1
+ import { a as auth_schema_exports, d as user$1, i as apikey, n as createDatabase, r as account, t as readConfig, u as session } from "./config-BQw6mPzE.mjs";
2
+ import { A as hasAdminRole, B as memberSchema, Bt as searchSettingsSchema, C as errorSchema, Ft as instanceUpdateSchema, G as notificationReadInput, H as notificationIdsInput, I as isOperation, It as photoInputSchema, L as isPresetColor, Lt as photoResultSchema, M as importProviderDefinitions, Mt as adminInstanceSchema, Pt as brokerSettingsSchema, R as isPublicOperation, Rt as s3SettingsSchema, U as notificationPreferencesInput, W as notificationPreferencesSchema, Y as operations, at as publicOperations, c as credentialInput, g as defaultWorkflow, ht as scopes, j as importLimits, mt as roles, ot as pushDeviceInput, q as okSchema, s as closedStates, t as ApiError, wt as ticketExportFormat, xt as stateLabels } from "./src-BaTNDJB7.mjs";
3
3
  import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
4
- import { $ as stagedUploads, A as requireSession, B as attachmentContent, C as defaultSenders, D as createAccess, E as createHttpSender, F as decodeImage, G as events, H as blobObjects, I as imageResponse, J as notificationPreferences, L as stagedUploadCutoff, M as createTicketSearch, N as createTicketIndex, O as createWriteLimiter, P as createBlobStorage, Q as pushDevices, R as createObjectStore, S as receiveRepositoryWebhook, T as DeliveryError, U as deliveryCheckpoints, V as attachments, W as eventClock, X as profilePhotos, Y as notifications, Z as projects, _ as one, a as deliverySecrets, at as matchesOIDCBinding, b as requireRow, c as ownerBlocker, d as createDomain, et as storageBackends, f as errorEnvelope, g as notify, h as json, i as createInstance, it as workspaces, j as readCalendarFeed, k as requireInstanceAdmin, l as appendEvents, m as intakeOnly, n as emailCodesAvailable, nt as tickets, o as otherActiveAdministrator, p as fail, q as members, r as signInProviders, rt as workspaceMembers, s as ownedWorkspaces, t as createAuth, tt as subscriptions, u as assertVersion, v as reachable, w as deliverBatch, x as processImports, y as recipientColumns, z as createPollingJob } from "./auth-Cs_whzgk.mjs";
4
+ import { $ as stagedUploads, A as requireSession, B as attachmentContent, C as defaultSenders, D as createAccess, E as createHttpSender, F as decodeImage, G as events, H as blobObjects, I as imageResponse, J as notificationPreferences, L as stagedUploadCutoff, M as createTicketSearch, N as createTicketIndex, O as createWriteLimiter, P as createBlobStorage, Q as pushDevices, R as createObjectStore, S as receiveRepositoryWebhook, T as DeliveryError, U as deliveryCheckpoints, V as attachments, W as eventClock, X as profilePhotos, Y as notifications, Z as projects, _ as one, a as deliverySecrets, at as matchesOIDCBinding, b as requireRow, c as ownerBlocker, d as createDomain, et as storageBackends, f as errorEnvelope, g as notify, h as json, i as createInstance, it as workspaces, j as readCalendarFeed, k as requireInstanceAdmin, l as appendEvents, m as intakeOnly, n as emailCodesAvailable, nt as tickets, o as otherActiveAdministrator, p as fail, q as members, r as signInProviders, rt as workspaceMembers, s as ownedWorkspaces, t as createAuth, tt as subscriptions, u as assertVersion, v as reachable, w as deliverBatch, x as processImports, y as recipientColumns, z as createPollingJob } from "./auth-CFjetlB4.mjs";
5
5
  import { serve } from "@hono/node-server";
6
6
  import pg from "pg";
7
7
  import { drizzle } from "drizzle-orm/node-postgres";
@@ -359,7 +359,7 @@ function createCleanup() {
359
359
  }
360
360
  //#endregion
361
361
  //#region src/version.ts
362
- const applicationVersion = "0.5.0";
362
+ const applicationVersion = "0.6.1";
363
363
  //#endregion
364
364
  //#region src/realtime.ts
365
365
  async function createRealtime(config) {
@@ -3436,10 +3436,15 @@ async function initializeApplication(database, config, adapters, cleanup) {
3436
3436
  const result = await receiveRepositoryWebhook(domain, repositories, secrets, c.req.param("connectionId"), c.req.raw.headers, await c.req.text());
3437
3437
  return c.json(result, result.outcome === "duplicate" ? 200 : 202);
3438
3438
  });
3439
+ const intakeRoutes = [
3440
+ ["POST", /^\/api\/v1\/projects\/[^/]+\/tickets$/],
3441
+ ["POST", /^\/api\/v1\/projects\/[^/]+\/tickets\/[^/]+\/attachments$/],
3442
+ ["GET", /^\/api\/v1\/projects\/[^/]+\/tickets\/[^/]+\/attachments$/]
3443
+ ];
3439
3444
  app.use("/api/v1/*", async (c, next) => {
3440
3445
  access.checkOrigin(c.req.raw);
3441
3446
  const actor = await access.authenticate(c.req.raw);
3442
- if (intakeOnly(actor) && !(c.req.method === "POST" && /^\/api\/v1\/projects\/[^/]+\/tickets$/.test(c.req.path))) fail(403, "insufficient_scope", "Intake credentials can only create tickets");
3447
+ if (intakeOnly(actor) && !intakeRoutes.some(([method, path]) => c.req.method === method && path.test(c.req.path))) fail(403, "insufficient_scope", "Intake credentials can only create tickets and attach files to them");
3443
3448
  c.set("actor", actor);
3444
3449
  if (!actor.machine && ![
3445
3450
  "GET",
@@ -1,4 +1,4 @@
1
- import { n as createDatabase, t as readConfig } from "./config-DKuNQY5b.mjs";
1
+ import { n as createDatabase, t as readConfig } from "./config-BQw6mPzE.mjs";
2
2
  import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
3
3
  //#region src/cli/migrate.ts
4
4
  const database = createDatabase(readConfig());
@@ -1,6 +1,6 @@
1
- import { d as user, n as createDatabase, r as account, t as readConfig, u as session } from "./config-DKuNQY5b.mjs";
2
- import { A as hasAdminRole } from "./src-DptOUNRd.mjs";
3
- import { K as instanceSettings, p as fail, t as createAuth } from "./auth-Cs_whzgk.mjs";
1
+ import { d as user, n as createDatabase, r as account, t as readConfig, u as session } from "./config-BQw6mPzE.mjs";
2
+ import { A as hasAdminRole } from "./src-BaTNDJB7.mjs";
3
+ import { K as instanceSettings, p as fail, t as createAuth } from "./auth-CFjetlB4.mjs";
4
4
  import { and, eq, isNull, sql } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
  import { parseArgs } from "node:util";
@@ -1,5 +1,79 @@
1
1
  import { z } from "zod";
2
2
  import i18next from "i18next";
3
+ //#region ../../packages/contracts/src/icons.ts
4
+ const ticketSymbols = {
5
+ status: {
6
+ backlog: {
7
+ icon: "circle-dashed",
8
+ tone: "muted"
9
+ },
10
+ todo: {
11
+ icon: "circle",
12
+ tone: "muted"
13
+ },
14
+ in_progress: {
15
+ icon: "circle-dot",
16
+ tone: "accent"
17
+ },
18
+ in_review: {
19
+ icon: "clock",
20
+ tone: "warning"
21
+ },
22
+ done: {
23
+ icon: "circle-check",
24
+ tone: "success"
25
+ },
26
+ cancelled: {
27
+ icon: "circle-x",
28
+ tone: "muted"
29
+ }
30
+ },
31
+ priority: {
32
+ urgent: {
33
+ icon: "octagon-alert",
34
+ tone: "danger"
35
+ },
36
+ high: {
37
+ icon: "arrow-up",
38
+ tone: "warning"
39
+ },
40
+ medium: {
41
+ icon: "equal",
42
+ tone: "foreground"
43
+ },
44
+ low: {
45
+ icon: "arrow-down",
46
+ tone: "accent"
47
+ },
48
+ none: {
49
+ icon: "minus",
50
+ tone: "muted"
51
+ }
52
+ },
53
+ kind: {
54
+ feature: {
55
+ icon: "sparkles",
56
+ tone: "muted"
57
+ },
58
+ bug: {
59
+ icon: "bug",
60
+ tone: "muted"
61
+ },
62
+ chore: {
63
+ icon: "wrench",
64
+ tone: "muted"
65
+ },
66
+ research: {
67
+ icon: "flask-conical",
68
+ tone: "muted"
69
+ },
70
+ documentation: {
71
+ icon: "book-open",
72
+ tone: "muted"
73
+ }
74
+ }
75
+ };
76
+ //#endregion
3
77
  //#region ../../packages/contracts/src/instance.ts
4
78
  const endpoint$1 = z.url({ protocol: /^https?$/ }).refine((value) => !/^https?:\/\/[^/]*@/.test(value) && !/[?#]/.test(value), "Use an HTTP(S) endpoint without embedded credentials");
5
79
  const s3SettingsSchema = z.object({
@@ -930,6 +1004,7 @@ var en_US_default = {
930
1004
  },
931
1005
  ticket: {
932
1006
  list: {
1007
+ manageViews: "Manage views",
933
1008
  selectSection: "Select {{section}} tickets",
934
1009
  expandSection: "Expand {{section}} tickets",
935
1010
  collapseSection: "Collapse {{section}} tickets",
@@ -1058,6 +1133,7 @@ var en_US_default = {
1058
1133
  noDue: "No due date"
1059
1134
  },
1060
1135
  filter: {
1136
+ add: "Add filter",
1061
1137
  me: "Me",
1062
1138
  searchTickets: "Search tickets",
1063
1139
  searchTicketsPlaceholder: "Search tickets…",
@@ -1550,7 +1626,7 @@ var en_US_default = {
1550
1626
  permission: {
1551
1627
  intake: {
1552
1628
  name: "Submit reports",
1553
- description: "Create quarantined tickets only. Cannot read or edit tickets. Use alone."
1629
+ description: "Create quarantined tickets and attach files to them only. Cannot read or edit tickets. Use alone."
1554
1630
  },
1555
1631
  read: {
1556
1632
  name: "Read tickets",
@@ -2497,6 +2573,9 @@ var en_US_default = {
2497
2573
  }
2498
2574
  },
2499
2575
  native: {
2576
+ "navigation.projectButton": "{{project}} ⌄",
2577
+ "navigation.recentProjects": "Recent projects",
2578
+ "navigation.switchProject": "Switch workspace or project",
2500
2579
  labelValue: "{{label}}: {{value}}",
2501
2580
  ui: {
2502
2581
  dismiss: "Dismiss",
@@ -2545,6 +2624,10 @@ var en_US_default = {
2545
2624
  create: "Create project"
2546
2625
  },
2547
2626
  tickets: {
2627
+ options: "List options",
2628
+ expandAll: "Expand all sections",
2629
+ collapseAll: "Collapse all sections",
2630
+ select: "Select tickets",
2548
2631
  search: "Search tickets",
2549
2632
  searchPlaceholder: "Title, description, or key",
2550
2633
  allStatuses: "All statuses",
@@ -2671,6 +2754,7 @@ var en_US_default = {
2671
2754
  next: "Next activity"
2672
2755
  },
2673
2756
  inbox: {
2757
+ read: "Read",
2674
2758
  markAllRead: "Mark all read",
2675
2759
  empty: "No notifications"
2676
2760
  },
@@ -2855,6 +2939,7 @@ const attachmentLimit = 1e7;
2855
2939
  const stagedUploadLifetime = 864e5;
2856
2940
  const stateLabels = labelTable(states, (value) => t(`common.status.${value}`));
2857
2941
  const priorityLabels = labelTable(priorities, (value) => t(`common.priority.${value}`));
2942
+ ticketSymbols.status.backlog.tone, ticketSymbols.status.todo.tone, ticketSymbols.status.in_progress.tone, ticketSymbols.status.in_review.tone, ticketSymbols.status.done.tone, ticketSymbols.status.cancelled.tone;
2858
2943
  const id = z.string().min(1).max(80);
2859
2944
  const timestamp = z.iso.datetime();
2860
2945
  const version = z.number().int().positive();
@@ -4635,7 +4720,7 @@ const operations = {
4635
4720
  upload_attachment: op(ticketInput.extend({
4636
4721
  ...mutation,
4637
4722
  ...fileFields
4638
- }).strict(), attachmentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/attachments", "write", "Upload up to 10 MB of base64-encoded bytes. The original filename is metadata, never a filesystem path."),
4723
+ }).strict(), attachmentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/attachments", "write", "Upload up to 10 MB of base64-encoded bytes. The original filename is metadata, never a filesystem path. An intake credential can attach up to 10 files to a quarantined ticket it created, within 1 GB across its quarantined tickets."),
4639
4724
  stage_upload: op(projectInput.extend({
4640
4725
  ...mutation,
4641
4726
  ...fileFields
@@ -4644,7 +4729,7 @@ const operations = {
4644
4729
  ...mutation,
4645
4730
  uploadId: z.uuid()
4646
4731
  }).strict(), okSchema, "DELETE", "/projects/{projectId}/uploads/{uploadId}", "write", "Delete one of your unclaimed staged uploads and its bytes."),
4647
- list_attachments: op(ticketInput, z.array(attachmentSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/attachments", "read", "List authorized attachment metadata."),
4732
+ list_attachments: op(ticketInput, z.array(attachmentSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/attachments", "read", "List authorized attachment metadata. An intake credential can list them on a quarantined ticket it created."),
4648
4733
  delete_attachment: op(projectInput.extend({
4649
4734
  ...mutation,
4650
4735
  attachmentId: z.uuid()
@@ -4714,7 +4799,7 @@ const operations = {
4714
4799
  }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/release", "claim", "Release the caller's claim."),
4715
4800
  recover_claim: op(ticketInput.extend({
4716
4801
  ...mutation,
4717
- claimId: z.uuid(),
4802
+ claimId: z.uuid().optional(),
4718
4803
  reason: text.min(10).max(2e3),
4719
4804
  confirmedStopped: z.literal(true),
4720
4805
  cancelActive: z.boolean().optional(),
@@ -4723,7 +4808,10 @@ const operations = {
4723
4808
  }).strict().refine((input) => !input.reschedule || input.expectedVersion !== void 0, {
4724
4809
  message: "expectedVersion is required to reschedule recovered work",
4725
4810
  path: ["expectedVersion"]
4726
- }), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/recover", "claim", "Recover an expired claim after confirming the worker stopped. Optionally reschedule it to To do at its expectedVersion. Project managers with admin scope may explicitly cancel an active claim."),
4811
+ }).refine((input) => input.claimId !== void 0 || input.expectedVersion !== void 0, {
4812
+ message: "expectedVersion is required to recover ownership without a claim",
4813
+ path: ["expectedVersion"]
4814
+ }), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/recover", "claim", "Recover an expired claim after confirming the worker stopped. Optionally reschedule it to To do at its expectedVersion. Project managers with admin scope may explicitly cancel an active claim. Omit claimId, with the ticket's expectedVersion, to lift the ownership requirement of a ticket that has no claim left, such as one released outside To do."),
4727
4815
  set_subscription: op(ticketInput.extend({
4728
4816
  ...mutation,
4729
4817
  enabled: z.boolean()
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "tickets" ADD COLUMN "intake_credential_id" text;--> statement-breakpoint
2
+ CREATE INDEX "ticket_intake_credential" ON "tickets" USING btree ("intake_credential_id") WHERE "tickets"."intake_credential_id" IS NOT NULL;