@pegma/support-desk-application 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { createEngineeringFollowupSourceServices, planEngineeringFollowupSourceC
16
16
  import { createEngineeringFollowupWorkServices } from "./engineering-followup-work.js";
17
17
  import { fields as followupFields, integer as followupInteger, text as followupText, } from "./assistant-turn-records.js";
18
18
  import { engineeringFollowupCommandKey, engineeringFollowupHeadKey, safeEngineeringFollowupSnapshot as followupSnapshot, checkEngineeringFollowupRecordCapacity, } from "./engineering-followup-records.js";
19
- import { engineeringFollowupNow, engineeringFollowupVersionsCurrent, } from "./engineering-followup-source.js";
19
+ import { engineeringFollowupNow, engineeringFollowupTrusted, engineeringFollowupVersionsCurrent, } from "./engineering-followup-source.js";
20
20
  import { assertResolutionLearningRecord, resolveResolutionLearningOptions, } from "./resolution-learning-records.js";
21
21
  import { createResolutionLearningServices, createResolutionLearningSource, planResolutionLearningTicketMutation, } from "./resolution-learning.js";
22
22
  export { resolutionLearningDeliveryBinding, resolutionLearningManifestHash, } from "./resolution-learning-worker.js";
@@ -861,6 +861,65 @@ function snapshotCreateCustomerTicketCommand(input) {
861
861
  ...(initialHandling === undefined ? {} : { initialHandling }),
862
862
  });
863
863
  }
864
+ function snapshotCustomerTicketAdmission(input) {
865
+ const source = boundaryObject(input, "admitted create command");
866
+ const allowed = [
867
+ "commandId",
868
+ "correlationId",
869
+ "ticketId",
870
+ "messageId",
871
+ "subject",
872
+ "body",
873
+ "category",
874
+ "requesterEmail",
875
+ "preference",
876
+ "disclosureVersion",
877
+ ];
878
+ const keys = Reflect.ownKeys(source);
879
+ if (Array.isArray(source) ||
880
+ keys.some((key) => typeof key !== "string" || !allowed.includes(key)))
881
+ throw new TypeError("Invalid admitted create command fields");
882
+ const command = Object.freeze(Object.fromEntries(keys.map((key) => [
883
+ key,
884
+ ownDataProperty(source, key, "admitted create command field"),
885
+ ])));
886
+ const preference = ownDataProperty(command, "preference", "preference");
887
+ if (preference !== "human" && preference !== "assistant_if_available")
888
+ throw new TypeError("Invalid admitted create preference");
889
+ const raw = ownDataProperty(command, "disclosureVersion", "disclosureVersion", true);
890
+ const disclosureVersion = raw === undefined ? undefined : boundaryString(raw, "disclosureVersion");
891
+ if (disclosureVersion !== undefined)
892
+ requireIdentifier(disclosureVersion, "disclosureVersion");
893
+ return {
894
+ command,
895
+ intent: Object.freeze({
896
+ preference,
897
+ ...(disclosureVersion === undefined ? {} : { disclosureVersion }),
898
+ }),
899
+ };
900
+ }
901
+ function customerCreateMethods(create) {
902
+ return {
903
+ async createCustomerTicket(access, input) {
904
+ return (await create(access, input)).view;
905
+ },
906
+ async createAdmittedCustomerTicket(access, input, options) {
907
+ requirePermission(access, supportPermissions.create);
908
+ requirePermission(access, supportPermissions.readOwn);
909
+ const { command, intent } = snapshotCustomerTicketAdmission(input);
910
+ return create(access, command, {
911
+ intent,
912
+ ...(options === undefined ? {} : { options }),
913
+ });
914
+ },
915
+ };
916
+ }
917
+ function admittedCreateMode(record) {
918
+ if (record?.kind !== "command" ||
919
+ (record.initialMode !== "human" && record.initialMode !== "assistant"))
920
+ throw new SupportDeskConflictError("Admitted create receipt mode is invalid");
921
+ return record.initialMode;
922
+ }
864
923
  function snapshotReplyToCustomerTicketCommand(input) {
865
924
  const source = boundaryObject(input, "reply command");
866
925
  const raw = {
@@ -2408,9 +2467,12 @@ export function createSupportDeskApplication(options) {
2408
2467
  };
2409
2468
  const engineeringFollowupSource = createEngineeringFollowupSourceServices(followupDependencies);
2410
2469
  const engineeringFollowupWork = createEngineeringFollowupWorkServices(followupDependencies, engineeringFollowupSource.internal.load);
2411
- const approveAndPostEngineeringFollowupWebUpdate = async (access, input, guard) => {
2470
+ const approveAndPostEngineeringFollowupWebUpdate = async (access, input, guard, options = {}) => {
2412
2471
  const actorId = access.principalId;
2472
+ const { signal, verifyBeforeCommit } = options;
2413
2473
  const authorize = () => {
2474
+ if (signal?.aborted)
2475
+ throw new SupportDeskConflictError("Follow-up posting cancelled");
2414
2476
  requireIdentifier(access.principalId, "principalId");
2415
2477
  if (access.principalId !== actorId)
2416
2478
  throw new SupportDeskAuthorizationError(supportPermissions.engineeringFollowupReview);
@@ -2495,7 +2557,7 @@ export function createSupportDeskApplication(options) {
2495
2557
  requireNonempty(body, "body");
2496
2558
  enforceLimit(body, "body", limits.maxMessageCharacters);
2497
2559
  const bodyHash = await requestFingerprint({ body });
2498
- await guard();
2560
+ const assertProofCurrent = await guard();
2499
2561
  authorize();
2500
2562
  const occurredAt = clampTicketTime(clock.now(), ticket.updatedAt);
2501
2563
  const updated = applyTicketEvent(ticket, {
@@ -2566,8 +2628,22 @@ export function createSupportDeskApplication(options) {
2566
2628
  if (!(await engineeringFollowupVersionsCurrent(followupDependencies, current.loaded, current.work, current.engineeringWork)))
2567
2629
  continue;
2568
2630
  authorize();
2569
- if (now() >=
2570
- Math.min(Date.parse(work.deadline), checked.deadline, Date.parse(current.proof.attestation.validUntil)))
2631
+ const proofDeadline = Math.min(Date.parse(work.deadline), checked.deadline, Date.parse(current.proof.attestation.validUntil));
2632
+ if (now() >= proofDeadline)
2633
+ throw new SupportDeskConflictError("Follow-up posting proof expired");
2634
+ if (verifyBeforeCommit !== undefined) {
2635
+ const authorityDeadline = Math.min(proofDeadline, now() + engineeringFollowupOptions.trustTimeoutMilliseconds);
2636
+ await engineeringFollowupTrusted(followupDependencies, async () => {
2637
+ authorize();
2638
+ await verifyBeforeCommit();
2639
+ }, signal);
2640
+ authorize();
2641
+ if (now() >= authorityDeadline)
2642
+ throw new SupportDeskConflictError("Follow-up posting authority or proof expired");
2643
+ }
2644
+ if (assertProofCurrent !== undefined &&
2645
+ (typeof assertProofCurrent !== "function" ||
2646
+ assertProofCurrent() !== true))
2571
2647
  throw new SupportDeskConflictError("Follow-up posting proof expired");
2572
2648
  const outcome = await records.transact(command.ticketId, [
2573
2649
  ...derivations.actions,
@@ -2663,7 +2739,7 @@ export function createSupportDeskApplication(options) {
2663
2739
  : { assistantTurnFingerprint: assistantTurnCommit.fingerprint }),
2664
2740
  });
2665
2741
  const completion = assistantTurnCommit;
2666
- const parallel = completion?.kind === "complete";
2742
+ const parallel = completion?.kind === "complete" || completion?.kind === "finish";
2667
2743
  for (let attempt = 0; attempt < maxConflictAttempts; attempt += 1) {
2668
2744
  const ticketRead = records.getVersioned(ticketKey(command.ticketId));
2669
2745
  const duplicateRead = parallel
@@ -2692,8 +2768,8 @@ export function createSupportDeskApplication(options) {
2692
2768
  ticket.revision !== command.expectedRevision ||
2693
2769
  effectiveTicketHandling(ticket).generation !==
2694
2770
  command.expectedHandlingGeneration)) {
2695
- // The speculative receipt may predate a concurrent exact completion
2696
- // that this Ticket observation already contains. Recover its receipt
2771
+ // The speculative receipt may predate a concurrent completion or
2772
+ // finish already reflected in this Ticket. Recover its exact receipt
2697
2773
  // before rejecting the now-stale live fence; ordinary paired reads
2698
2774
  // retain their existing cost and original whole-attempt CAS versions.
2699
2775
  const accepted = await records.get(commandKey(command.ticketId, command.commandId));
@@ -3106,19 +3182,63 @@ export function createSupportDeskApplication(options) {
3106
3182
  .sort((a, b) => a.revision - b.revision || a.commandId.localeCompare(b.commandId))
3107
3183
  .map(({ kind: _kind, partition: _partition, id: _id, ...proof }) => Object.freeze(proof));
3108
3184
  },
3109
- async createCustomerTicket(access, input) {
3185
+ ...customerCreateMethods(async (access, input, admitted) => {
3110
3186
  requirePermission(access, supportPermissions.create);
3111
3187
  requireIdentifier(access.principalId, "principalId");
3188
+ const actorId = access.principalId;
3189
+ const principal = () => admitted === undefined ? access.principalId : actorId;
3190
+ const signal = admitted?.options?.signal;
3191
+ function authorizeAdmitted(current = access, assistant = false) {
3192
+ if (admitted === undefined)
3193
+ return;
3194
+ if (signal?.aborted)
3195
+ throw new SupportDeskConflictError("Admitted create was cancelled");
3196
+ if (current.principalId !== actorId)
3197
+ throw new SupportDeskAuthorizationError(supportPermissions.create);
3198
+ requirePermission(current, supportPermissions.create);
3199
+ requirePermission(current, supportPermissions.readOwn);
3200
+ if (assistant)
3201
+ requirePermission(current, supportPermissions.assistantStart);
3202
+ }
3203
+ async function waitForAdmission(check) {
3204
+ authorizeAdmitted();
3205
+ if (signal === undefined)
3206
+ return check();
3207
+ let onAbort;
3208
+ try {
3209
+ return await Promise.race([
3210
+ Promise.resolve().then(() => {
3211
+ authorizeAdmitted();
3212
+ return check();
3213
+ }),
3214
+ new Promise((_resolve, reject) => {
3215
+ onAbort = () => reject(new SupportDeskConflictError("Admitted create was cancelled"));
3216
+ signal.addEventListener("abort", onAbort, { once: true });
3217
+ if (signal.aborted)
3218
+ onAbort();
3219
+ }),
3220
+ ]);
3221
+ }
3222
+ finally {
3223
+ if (onAbort !== undefined)
3224
+ signal.removeEventListener("abort", onAbort);
3225
+ }
3226
+ }
3227
+ authorizeAdmitted();
3112
3228
  const command = snapshotCreateCustomerTicketCommand(input);
3113
- if (command.initialHandling === "assistant") {
3229
+ let initialHandling = command.initialHandling;
3230
+ if (admitted === undefined && initialHandling === "assistant") {
3114
3231
  requirePermission(access, supportPermissions.assistantStart);
3115
3232
  }
3116
3233
  requireIdentifier(command.ticketId, "ticketId");
3117
3234
  requireIdentifier(command.messageId, "messageId");
3118
3235
  requireIdentifier(command.commandId, "commandId");
3119
3236
  requireIdentifier(command.correlationId, "correlationId");
3120
- enforceLimit(command.subject, "subject", limits.maxSubjectCharacters);
3121
- enforceLimit(command.body, "body", limits.maxMessageCharacters);
3237
+ // Admitted replay keeps fixed ceilings when later instance limits shrink.
3238
+ // Fresh admission applies the configured limits after receipt recovery.
3239
+ const inputLimits = admitted === undefined ? limits : supportDeskLimitMaximums;
3240
+ enforceLimit(command.subject, "subject", inputLimits.maxSubjectCharacters);
3241
+ enforceLimit(command.body, "body", inputLimits.maxMessageCharacters);
3122
3242
  const fingerprintPayload = {
3123
3243
  type: "create_customer_ticket",
3124
3244
  ticketId: command.ticketId,
@@ -3135,7 +3255,20 @@ export function createSupportDeskApplication(options) {
3135
3255
  ? {}
3136
3256
  : { initialHandling: command.initialHandling }),
3137
3257
  };
3138
- const fingerprint = await requestFingerprint(fingerprintPayload);
3258
+ const commandType = admitted === undefined
3259
+ ? "create_customer_ticket"
3260
+ : "create_admitted_customer_ticket";
3261
+ const fingerprint = await requestFingerprint(admitted === undefined
3262
+ ? fingerprintPayload
3263
+ : {
3264
+ ...fingerprintPayload,
3265
+ type: commandType,
3266
+ version: 1,
3267
+ actorId,
3268
+ commandId: command.commandId,
3269
+ preference: admitted.intent.preference,
3270
+ disclosureVersion: admitted.intent.disclosureVersion ?? null,
3271
+ });
3139
3272
  const createThreading = {
3140
3273
  ticketNumber: true,
3141
3274
  ...(command.notification === undefined
@@ -3146,8 +3279,10 @@ export function createSupportDeskApplication(options) {
3146
3279
  // later config change cannot break idempotent retries of the same command.
3147
3280
  // Replay never reserves another instance ticket number.
3148
3281
  const existingCreate = await records.get(commandKey(command.ticketId, command.commandId));
3149
- let createIsReplay = duplicateMatches(existingCreate, "create_customer_ticket", command.messageId, fingerprint);
3150
- if (!createIsReplay &&
3282
+ authorizeAdmitted();
3283
+ let createIsReplay = duplicateMatches(existingCreate, commandType, command.messageId, fingerprint);
3284
+ if (admitted === undefined &&
3285
+ !createIsReplay &&
3151
3286
  command.initialHandling === undefined &&
3152
3287
  existingCreate?.kind === "command" &&
3153
3288
  existingCreate.commandType === "create_customer_ticket" &&
@@ -3174,19 +3309,28 @@ export function createSupportDeskApplication(options) {
3174
3309
  }
3175
3310
  if (createIsReplay) {
3176
3311
  const current = await records.get(ticketKey(command.ticketId));
3177
- if (current?.kind !== "ticket" ||
3178
- !owns(current.ticket, access.principalId))
3312
+ authorizeAdmitted();
3313
+ if (current?.kind !== "ticket" || !owns(current.ticket, principal()))
3179
3314
  throw new SupportDeskNotFoundError();
3315
+ const initialMode = admitted === undefined
3316
+ ? (initialHandling ?? "human")
3317
+ : admittedCreateMode(existingCreate);
3180
3318
  // A crash after ticket commit and before index confirmation must still
3181
3319
  // converge the principal hint on retry.
3182
3320
  const fence = await records.get(ticketReservationKey(command.ticketId));
3321
+ authorizeAdmitted();
3183
3322
  if (fence?.kind === "ticket_reservation" &&
3184
3323
  fence.state === "committed") {
3185
- await confirmCustomerTicket(access.principalId, command.ticketId, fence.token, fence.generation);
3324
+ await confirmCustomerTicket(principal(), command.ticketId, fence.token, fence.generation);
3186
3325
  }
3187
- return customerViewAfterChange(access.principalId, command.ticketId, createThreading);
3326
+ authorizeAdmitted();
3327
+ const view = await customerViewAfterChange(principal(), command.ticketId, createThreading);
3328
+ authorizeAdmitted();
3329
+ return { view, initialMode };
3188
3330
  }
3189
- if (command.initialHandling === "assistant" && !assistantHandlingEnabled)
3331
+ if (admitted !== undefined && existingCreate !== null)
3332
+ throw new SupportDeskConflictError("Admitted create command changed");
3333
+ if (initialHandling === "assistant" && !assistantHandlingEnabled)
3190
3334
  throw new SupportDeskConflictError("Assistant handling is disabled");
3191
3335
  if (command.category !== undefined) {
3192
3336
  if (allowedCategories === undefined ||
@@ -3194,6 +3338,29 @@ export function createSupportDeskApplication(options) {
3194
3338
  throw new TypeError("create command.category is not configured for this Support Desk instance");
3195
3339
  }
3196
3340
  }
3341
+ const verifyBeforeCommit = admitted?.options?.verifyBeforeCommit;
3342
+ let assistantAccess;
3343
+ if (admitted !== undefined) {
3344
+ enforceLimit(command.subject, "subject", limits.maxSubjectCharacters);
3345
+ enforceLimit(command.body, "body", limits.maxMessageCharacters);
3346
+ if (typeof verifyBeforeCommit !== "function")
3347
+ throw new SupportDeskConflictError("Fresh create admission is required");
3348
+ const resolveAssistantStart = admitted.options?.resolveAssistantStart;
3349
+ if (admitted.intent.preference === "assistant_if_available" &&
3350
+ admitted.intent.disclosureVersion !== undefined &&
3351
+ assistantHandlingEnabled &&
3352
+ assistantTurnOptions !== undefined &&
3353
+ resolveAssistantStart !== undefined) {
3354
+ const current = await waitForAdmission(resolveAssistantStart);
3355
+ authorizeAdmitted();
3356
+ if (current !== null) {
3357
+ authorizeAdmitted(current, true);
3358
+ assistantAccess = current;
3359
+ initialHandling = "assistant";
3360
+ }
3361
+ }
3362
+ }
3363
+ let initialMode = initialHandling ?? "human";
3197
3364
  // Fail closed before durable number/index reservation so a too-small
3198
3365
  // partition budget cannot exhaust the principal ticket index with
3199
3366
  // reserved-but-never-created tickets.
@@ -3201,18 +3368,17 @@ export function createSupportDeskApplication(options) {
3201
3368
  (resolutionLearningOptions.enabled ? 1 : 0) +
3202
3369
  (engineeringOptions.enabled ? 1 : 0) +
3203
3370
  (command.notification === undefined ? 0 : 1) +
3204
- (assistantTurnOptions !== undefined &&
3205
- command.initialHandling === "assistant"
3371
+ (assistantTurnOptions !== undefined && initialHandling === "assistant"
3206
3372
  ? 1
3207
3373
  : 0);
3208
3374
  enforcePartitionRecordLimit(0, limits.maxRecordsPerTicket, estimatedCreateRows +
3209
3375
  (resolutionLearningOptions.enabled ? 2 : 0) +
3210
3376
  (engineeringOptions.enabled ? 2 : 0) +
3211
- (command.initialHandling === "assistant"
3377
+ (initialHandling === "assistant"
3212
3378
  ? assistantHumanReserveRows +
3213
3379
  (resolutionLearningOptions.enabled ? 1 : 0)
3214
3380
  : 0));
3215
- if (command.initialHandling === "assistant" &&
3381
+ if (initialHandling === "assistant" &&
3216
3382
  limits.maxMessagesPerTicket < 1 + assistantHumanReserveMessages)
3217
3383
  throw new SupportDeskLimitError("ticket_messages", limits.maxMessagesPerTicket);
3218
3384
  // Reserve outside the ticket transaction; gaps are accepted if create fails.
@@ -3228,15 +3394,13 @@ export function createSupportDeskApplication(options) {
3228
3394
  number: ticketNumber,
3229
3395
  subject: command.subject,
3230
3396
  channel: "web",
3231
- ...(command.initialHandling === undefined
3232
- ? {}
3233
- : { initialHandling: command.initialHandling }),
3397
+ ...(initialHandling === undefined ? {} : { initialHandling }),
3234
3398
  ...(command.category === undefined
3235
3399
  ? {}
3236
3400
  : { category: command.category }),
3237
3401
  requester: {
3238
3402
  association: "authenticated",
3239
- principalId: access.principalId,
3403
+ principalId: principal(),
3240
3404
  ...(command.requesterEmail === undefined
3241
3405
  ? {}
3242
3406
  : { email: command.requesterEmail }),
@@ -3247,22 +3411,21 @@ export function createSupportDeskApplication(options) {
3247
3411
  id: command.messageId,
3248
3412
  ticketId: command.ticketId,
3249
3413
  authorKind: "customer",
3250
- authorPrincipalId: access.principalId,
3414
+ authorPrincipalId: principal(),
3251
3415
  channel: "web",
3252
3416
  visibility: "customer",
3253
3417
  format: "plain_text",
3254
3418
  body: command.body,
3255
3419
  createdAt: now,
3256
3420
  };
3257
- const assistantTurn = assistantTurnOptions !== undefined &&
3258
- command.initialHandling === "assistant"
3421
+ const assistantTurn = assistantTurnOptions !== undefined && initialHandling === "assistant"
3259
3422
  ? await createAssistantTurnRecord(ticket, message, command.correlationId, assistantTurnOptions)
3260
3423
  : undefined;
3261
3424
  const notificationJob = notification === undefined
3262
3425
  ? undefined
3263
3426
  : deliveryJobAction(command.ticketId, command.messageId, now, notification);
3264
3427
  const notificationContent = deliveryContent(notification);
3265
- const reservation = await reserveCustomerTicket(access.principalId, command.ticketId, now);
3428
+ const reservation = await reserveCustomerTicket(principal(), command.ticketId, now);
3266
3429
  const existingReservation = await records.getVersioned(ticketReservationKey(command.ticketId));
3267
3430
  if (existingReservation?.value.kind === "ticket_reservation" &&
3268
3431
  existingReservation.value.state === "committed" &&
@@ -3332,7 +3495,7 @@ export function createSupportDeskApplication(options) {
3332
3495
  partition: command.ticketId,
3333
3496
  id: "quota",
3334
3497
  messageCount: 1,
3335
- ...(command.initialHandling === "assistant"
3498
+ ...(initialHandling === "assistant"
3336
3499
  ? { assistantManaged: true }
3337
3500
  : {}),
3338
3501
  },
@@ -3356,7 +3519,7 @@ export function createSupportDeskApplication(options) {
3356
3519
  correlationId: command.correlationId,
3357
3520
  ticketId: command.ticketId,
3358
3521
  revision: ticket.revision,
3359
- actorId: access.principalId,
3522
+ actorId: principal(),
3360
3523
  occurredAt: now,
3361
3524
  action: supportTicketAuditActions.created,
3362
3525
  }),
@@ -3367,10 +3530,11 @@ export function createSupportDeskApplication(options) {
3367
3530
  partition: command.ticketId,
3368
3531
  id: `command:${command.commandId}`,
3369
3532
  commandId: command.commandId,
3370
- commandType: "create_customer_ticket",
3533
+ commandType,
3371
3534
  messageId: command.messageId,
3372
3535
  requestFingerprint: fingerprint,
3373
3536
  completedAt: now,
3537
+ ...(admitted === undefined ? {} : { initialMode }),
3374
3538
  },
3375
3539
  },
3376
3540
  ...(notificationJob === undefined ? [] : [notificationJob]),
@@ -3381,16 +3545,33 @@ export function createSupportDeskApplication(options) {
3381
3545
  // Create starts an empty partition; fail closed if the first write would
3382
3546
  // exceed the configured physical-record budget.
3383
3547
  enforcePartitionRecordLimit(0, limits.maxRecordsPerTicket, actions.length);
3548
+ if (admitted !== undefined && verifyBeforeCommit !== undefined) {
3549
+ authorizeAdmitted();
3550
+ const assertCurrent = await waitForAdmission(() => verifyBeforeCommit(initialMode));
3551
+ authorizeAdmitted();
3552
+ if (assistantAccess !== undefined)
3553
+ authorizeAdmitted(assistantAccess, true);
3554
+ if (typeof assertCurrent !== "function" || assertCurrent() !== true)
3555
+ throw new SupportDeskConflictError("Admitted create authority changed");
3556
+ }
3384
3557
  const outcome = await records.transact(command.ticketId, actions);
3385
3558
  if (!outcome.committed) {
3386
3559
  const existing = await records.get(commandKey(command.ticketId, command.commandId));
3387
- if (!duplicateMatches(existing, "create_customer_ticket", command.messageId, fingerprint)) {
3560
+ if (!duplicateMatches(existing, commandType, command.messageId, fingerprint)) {
3388
3561
  throw new SupportDeskConflictError();
3389
3562
  }
3563
+ // An exact competing commit recovers the winner's historical mode,
3564
+ // using the same create/read-own authority as the early receipt path.
3565
+ if (admitted !== undefined)
3566
+ initialMode = admittedCreateMode(existing);
3390
3567
  }
3391
- await confirmCustomerTicket(access.principalId, command.ticketId, reservation.reservationToken, reservation.reservationGeneration);
3392
- return customerViewAfterChange(access.principalId, command.ticketId, createThreading);
3393
- },
3568
+ authorizeAdmitted();
3569
+ await confirmCustomerTicket(principal(), command.ticketId, reservation.reservationToken, reservation.reservationGeneration);
3570
+ authorizeAdmitted();
3571
+ const view = await customerViewAfterChange(principal(), command.ticketId, createThreading);
3572
+ authorizeAdmitted();
3573
+ return { view, initialMode };
3574
+ }),
3394
3575
  async replyToCustomerTicket(access, input) {
3395
3576
  requirePermission(access, supportPermissions.replyOwn);
3396
3577
  requireIdentifier(access.principalId, "principalId");