@go-labs-sg/rsvp 1.3.0 → 1.4.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 (3) hide show
  1. package/README.md +82 -7
  2. package/dist/index.js +90 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -108,7 +108,7 @@ rsvp event seating draft preview --organization-id org_123 --event-id evt_123 \
108
108
  --expected-version 1 --ack-sensitive-data
109
109
  rsvp event seating draft publish --organization-id org_123 --event-id evt_123 \
110
110
  --expected-version 1 --request-id 123e4567-e89b-42d3-a456-426614174000 \
111
- --reason 'Seat the confirmed guest' --allow-state-change --target evt_123
111
+ --reason 'Seat the confirmed guest' --allow-state-change --allow-email --target evt_123
112
112
  ```
113
113
 
114
114
  Save and Publish require the latest `expectedVersion`. A successful Publish applies
@@ -116,12 +116,37 @@ all changed guests atomically and clears only that version of your draft. Resolv
116
116
  preview issues before publishing; conflicts leave all bookings and the draft intact.
117
117
  For an unknown outcome, retry the same UUID `requestId`, version, reason and email
118
118
  choice: the durable receipt returns the previous counts without sending again or
119
- clearing a newer draft. A different intent needs a new request ID. Email defaults
120
- off. To opt in, use `--allow-email` and a full JSON payload including organizationId,
121
- eventId, expectedVersion, requestId, reason and `sendSeatChangeEmail: true`. Do not
122
- combine `--payload` with command-local version/reason/request flags;
123
- the server additionally requires notice permission and enabled recipient policy.
124
- Preview counts recipient registrations, not guests. On realtime-enabled servers,
119
+ clearing a newer draft. A different intent needs a new request ID. Preserve the
120
+ exact `seatChangeEmailRegistrationIds` set on retry (order is immaterial); changing
121
+ it, including changing between omitted and `[]`, rejects reuse of the request ID.
122
+
123
+ Preview returns `noticeRecipients`: one row per affected registration, with a
124
+ registration ID, a grant-scoped name, `eligible`, and an `ineligibleReason` when
125
+ unavailable. No email addresses are exposed by this seating response. Review both
126
+ eligible and ineligible rows before choosing who receives mail; several moving
127
+ party members still produce only one recipient registration.
128
+
129
+ Use `seatChangeEmailRegistrationIds` in a full `--payload` to choose the exact
130
+ eligible affected registrations. Leave a registration out to publish its seat
131
+ changes without email. `[]` explicitly sends no email, **including when the event
132
+ has automatic seat-change notifications**. Selecting an unaffected registration,
133
+ an ineligible recipient or duplicate IDs is rejected before publishing. The API
134
+ rechecks permissions and eligibility under the publish transaction, not just in
135
+ preview. Sending requires notice permission. An explicit empty array does not.
136
+
137
+ ```sh
138
+ rsvp event seating draft publish --allow-state-change --allow-email --target evt_123 \
139
+ --payload '{"organizationId":"org_123","eventId":"evt_123","expectedVersion":1,"requestId":"123e4567-e89b-42d3-a456-426614174000","reason":"Seat the confirmed party","sendSeatChangeEmail":true,"seatChangeEmailRegistrationIds":["reg_123"]}'
140
+ ```
141
+
142
+ Do not combine `--payload` with command-local version/reason/request flags. If the
143
+ recipient field is omitted, existing behavior remains: `sendSeatChangeEmail: true`
144
+ requests all affected registrations, otherwise the automatic event policy still
145
+ applies. Every draft publish conservatively requires CLI `--allow-email` (or the
146
+ interactive effects confirmation), even for explicit `[]`; it approves an effect
147
+ but does not enable sending. Use the matching server/client revision for explicit
148
+ recipient choices; older servers reject the new field rather than silently
149
+ ignoring it. Preview counts recipient registrations, not guests. On realtime-enabled servers,
125
150
  free new targets require validated per-registration holds at Publish, supplied as
126
151
  `seatHolds: [{registrationId, holds: [{seatMapId, clientId, holdToken}]}]` in the
127
152
  payload. Acquire these through the existing organizer hold-token flow; Stage never
@@ -537,6 +562,56 @@ it for the active registration/edit definition. Deploy the updated server before
537
562
  saving these settings because older validators discard unsupported fields.
538
563
  No database schema migration or attendee backfill is required.
539
564
 
565
+ ## Confirmation email layout, name, QR codes and attachments
566
+
567
+ In the form builder's **Flow settings → Confirmation email layout**, or through
568
+ `definition.communications.participant.email` with `event flow draft save`,
569
+ customise the participant confirmation email for every outcome of the form:
570
+
571
+ ```json
572
+ {
573
+ "communications": {
574
+ "participant": {
575
+ "email": {
576
+ "heading": "Hello {{name}}!",
577
+ "eventLine": null,
578
+ "confirmationButtonLabel": "View my Quest pass",
579
+ "supportFooter": "Questions? Email quest@example.com",
580
+ "showEventDetails": false,
581
+ "includeCheckInQrCodes": true,
582
+ "attachments": []
583
+ }
584
+ }
585
+ }
586
+ }
587
+ ```
588
+
589
+ - `{{name}}` works in template `subject`, `body`, `bodyRichText` and every
590
+ text field above. It uses the primary attendee's name, then the invitee's
591
+ name, and falls back to `there`.
592
+ - `heading`, `eventLine`, `confirmationButtonLabel` and `supportFooter`: omit to
593
+ keep the built-in text, `null` to hide that part, or a string to replace it.
594
+ A custom or hidden `supportFooter` also replaces the resubmission footer.
595
+ - `showEventDetails` (default `true`) controls the When/Where lines.
596
+ - `includeCheckInQrCodes` (default `false`) embeds each attendee's check-in QR
597
+ image once the event's check-in QR codes are released; before release the
598
+ email keeps the release message.
599
+ - `attachments`: up to 5 PDF, PNG or JPEG files, 10 MB each and 15 MB total.
600
+ Create an upload with `event flow confirmation attachment upload create`
601
+ (`organizationId`, `eventId`, `filename`, `contentType`, `byteSize`), `PUT`
602
+ the file unchanged to the returned `uploadUrl` with its `Content-Type`, then
603
+ add the returned `attachment` object to the list. Preview/publish reject keys
604
+ not uploaded for the same event, and publish verifies each uploaded object's
605
+ type and size.
606
+
607
+ Merge these fields into the complete current definition and publish as for
608
+ other form settings. Omitting `email` keeps existing emails unchanged. Deploy
609
+ the updated server before saving these settings. No database migration is
610
+ required.
611
+
612
+ `event flow preview` sends the definition as a POST body so large formatted
613
+ emails no longer exceed URL limits; it requires a server with this change.
614
+
540
615
  ## Administrator registration for an invitee
541
616
 
542
617
  Organizers can register someone on their behalf at any time, including after
package/dist/index.js CHANGED
@@ -7195,7 +7195,8 @@ var logoutCliSession = async ({
7195
7195
  // src/api-client.ts
7196
7196
  import {
7197
7197
  createTRPCProxyClient,
7198
- httpBatchLink
7198
+ httpBatchLink,
7199
+ splitLink
7199
7200
  } from "@trpc/client";
7200
7201
  import SuperJSON from "superjson";
7201
7202
 
@@ -7213,18 +7214,28 @@ var normalizeApiBaseUrl = (value) => {
7213
7214
  var getApiBaseUrl = (override) => normalizeApiBaseUrl(override ?? process.env.RSVP_API_URL ?? DEFAULT_RSVP_API_BASE_URL);
7214
7215
 
7215
7216
  // src/api-client.ts
7216
- var createRsvpClient = (apiBaseUrl = getApiBaseUrl(), accessToken) => createTRPCProxyClient({
7217
- links: [
7218
- httpBatchLink({
7219
- headers: () => accessToken ? {
7220
- authorization: `Bearer ${accessToken}`,
7221
- "x-trpc-source": "rsvp-cli"
7222
- } : { "x-trpc-source": "rsvp-cli" },
7223
- transformer: SuperJSON,
7224
- url: `${apiBaseUrl}/api/trpc`
7225
- })
7226
- ]
7227
- });
7217
+ var POST_QUERY_PATHS = new Set([
7218
+ "eventFlow.preview"
7219
+ ]);
7220
+ var createRsvpClient = (apiBaseUrl = getApiBaseUrl(), accessToken) => {
7221
+ const linkOptions = {
7222
+ headers: () => accessToken ? {
7223
+ authorization: `Bearer ${accessToken}`,
7224
+ "x-trpc-source": "rsvp-cli"
7225
+ } : { "x-trpc-source": "rsvp-cli" },
7226
+ transformer: SuperJSON,
7227
+ url: `${apiBaseUrl}/api/trpc`
7228
+ };
7229
+ return createTRPCProxyClient({
7230
+ links: [
7231
+ splitLink({
7232
+ condition: (op) => POST_QUERY_PATHS.has(op.path),
7233
+ true: httpBatchLink({ ...linkOptions, methodOverride: "POST" }),
7234
+ false: httpBatchLink(linkOptions)
7235
+ })
7236
+ ]
7237
+ });
7238
+ };
7228
7239
 
7229
7240
  // ../validators/src/assign-guests.ts
7230
7241
  var assignGuestFiltersInputSchema = object({
@@ -8030,7 +8041,53 @@ var confirmationBodyRichTextSchema = unknown().superRefine((value, ctx) => {
8030
8041
  message: "Confirmation body must contain 1\u201310,000 characters"
8031
8042
  });
8032
8043
  });
8044
+ var eventFlowConfirmationAttachmentContentTypes = [
8045
+ "application/pdf",
8046
+ "image/jpeg",
8047
+ "image/png"
8048
+ ];
8049
+ var maxEventFlowConfirmationAttachmentCount = 5;
8050
+ var maxEventFlowConfirmationAttachmentSize = 10 * 1024 * 1024;
8051
+ var maxEventFlowConfirmationAttachmentTotalSize = 15 * 1024 * 1024;
8052
+ var eventFlowConfirmationAttachmentFilenameSchema = string2().trim().min(1).max(120).regex(/^[^\\/:*?"<>|\r\n\t]+$/, "Attachment file names cannot contain path or control characters");
8053
+ var eventFlowConfirmationAttachmentSchema = strictObject({
8054
+ byteSize: number2().int().positive().max(maxEventFlowConfirmationAttachmentSize),
8055
+ contentType: _enum(eventFlowConfirmationAttachmentContentTypes),
8056
+ filename: eventFlowConfirmationAttachmentFilenameSchema,
8057
+ key: string2().trim().min(1).max(300)
8058
+ });
8059
+ var editableEmailCopySchema = (maxLength) => string2().trim().min(1).max(maxLength).nullable().optional();
8060
+ var eventFlowParticipantConfirmationEmailSchema = strictObject({
8061
+ attachments: array(eventFlowConfirmationAttachmentSchema).max(maxEventFlowConfirmationAttachmentCount).default([]),
8062
+ confirmationButtonLabel: editableEmailCopySchema(60),
8063
+ eventLine: editableEmailCopySchema(500),
8064
+ heading: editableEmailCopySchema(120),
8065
+ includeCheckInQrCodes: boolean2().default(false),
8066
+ showEventDetails: boolean2().default(true),
8067
+ supportFooter: editableEmailCopySchema(1000)
8068
+ }).superRefine((email, ctx) => {
8069
+ const totalSize = email.attachments.reduce((total, attachment) => total + attachment.byteSize, 0);
8070
+ if (totalSize > maxEventFlowConfirmationAttachmentTotalSize) {
8071
+ ctx.addIssue({
8072
+ code: "custom",
8073
+ message: "Confirmation attachments must total 15 MB or less",
8074
+ path: ["attachments"]
8075
+ });
8076
+ }
8077
+ const keys = new Set;
8078
+ for (const [index, attachment] of email.attachments.entries()) {
8079
+ if (keys.has(attachment.key)) {
8080
+ ctx.addIssue({
8081
+ code: "custom",
8082
+ message: "Each confirmation attachment can be added only once",
8083
+ path: ["attachments", index, "key"]
8084
+ });
8085
+ }
8086
+ keys.add(attachment.key);
8087
+ }
8088
+ });
8033
8089
  var participantConfirmationSchema = object({
8090
+ email: eventFlowParticipantConfirmationEmailSchema.optional(),
8034
8091
  enabled: boolean2(),
8035
8092
  includeResponseDetails: boolean2().default(false),
8036
8093
  replyTo: email2().optional(),
@@ -10390,7 +10447,10 @@ var eventTablePaginationSchema = object({
10390
10447
  var listEventInviteesInputSchema = organizationEventByIdInputSchema.extend({
10391
10448
  ...eventTablePaginationSchema.shape,
10392
10449
  audienceTierId: string2().min(1).nullable().optional(),
10393
- status: _enum(InviteeStatus).optional()
10450
+ status: _enum(InviteeStatus).optional(),
10451
+ statuses: array(_enum(InviteeStatus)).min(1).max(7).optional(),
10452
+ cursor: string2().min(1).max(64).optional(),
10453
+ direction: _enum(["NEXT", "PREVIOUS"]).default("NEXT")
10394
10454
  });
10395
10455
  var listEventRegistrationsInputSchema = organizationEventByIdInputSchema.extend({
10396
10456
  archive: _enum(["ACTIVE", "ARCHIVED", "ALL"]).default("ACTIVE"),
@@ -10865,6 +10925,11 @@ var createEventFlowImageAssetUploadInputSchema = organizationEventByIdInputSchem
10865
10925
  byteSize: number2().int().positive().max(maxEventFlowImageAssetSize),
10866
10926
  mimeType: eventFlowImageAssetMimeTypeSchema
10867
10927
  });
10928
+ var createEventFlowConfirmationAttachmentUploadInputSchema = organizationEventByIdInputSchema.extend({
10929
+ byteSize: number2().int().positive().max(maxEventFlowConfirmationAttachmentSize),
10930
+ contentType: _enum(eventFlowConfirmationAttachmentContentTypes),
10931
+ filename: eventFlowConfirmationAttachmentFilenameSchema
10932
+ });
10868
10933
  var finalizeEventFlowImageAssetInputSchema = organizationEventByIdInputSchema.extend({
10869
10934
  assetId: eventFlowImageAssetIdSchema
10870
10935
  });
@@ -10968,6 +11033,10 @@ var createEventFlowConsentVersionInputSchema = organizationEventByIdInputSchema.
10968
11033
  var setEventFlowGroupPasscodeInputSchema = organizationEventFlowInputSchema.extend({
10969
11034
  passcode: string2().trim().min(8).max(160)
10970
11035
  });
11036
+ var getEventFlowSeatCategoryColorsInputSchema = eventBySlugInputSchema.extend({
11037
+ flowVersionId: flowVersionIdSchema,
11038
+ seatMapId: flowVersionIdSchema
11039
+ }).strict();
10971
11040
  var issueEventFlowSeatHoldTokenInputSchema = eventBySlugInputSchema.extend({
10972
11041
  clientId: uuid2(),
10973
11042
  flowVersionId: flowVersionIdSchema,
@@ -11961,6 +12030,9 @@ var publishSeatingDraftInputSchema = seatingDraftScopeSchema.extend({
11961
12030
  requestId: uuid2(),
11962
12031
  reason: string2().trim().min(1).max(500),
11963
12032
  sendSeatChangeEmail: boolean2().default(false),
12033
+ seatChangeEmailRegistrationIds: array(string2().min(1)).max(100).refine((ids) => new Set(ids).size === ids.length, {
12034
+ message: "Each recipient registration may appear only once."
12035
+ }).optional(),
11964
12036
  seatHolds: array(object({
11965
12037
  registrationId: string2().min(1),
11966
12038
  holds: array(object({
@@ -12048,7 +12120,7 @@ var commands = {
12048
12120
  effects: [],
12049
12121
  operation: (api, input) => api.eventFlow.preview.query(input),
12050
12122
  payloadOnly: true,
12051
- summary: "Validate and preview an event flow, including date-limit messages (--payload JSON)."
12123
+ summary: "Validate and preview an event flow, including date-limit messages; the definition is sent as a POST body (--payload JSON)."
12052
12124
  },
12053
12125
  "event flow draft get": {
12054
12126
  effects: [],
@@ -12403,6 +12475,7 @@ Object.assign(commands, {
12403
12475
  "audit list": payloadCommand((api, input) => api.audit.listEventActivity.query(input), "List event audit activity (--payload JSON).", ["sensitive-data"]),
12404
12476
  "event check-in qr": payloadCommand((api, input) => api.event.checkInByQr.mutate(input), "Check in using a QR token (--payload JSON).", ["state-change"]),
12405
12477
  "event flow image upload create": payloadCommand((api, input) => api.eventFlow.createImageAssetUpload.mutate(input), "Create an event-local registration-flow image upload URL (--payload JSON).", ["external-write", "state-change"], "eventId"),
12478
+ "event flow confirmation attachment upload create": payloadCommand((api, input) => api.eventFlow.createConfirmationAttachmentUpload.mutate(input), "Create a confirmation-email attachment upload URL for a PDF, PNG or JPEG up to 10 MB (filename, contentType, byteSize); PUT the file there, then add the returned attachment to communications.participant.email.attachments (--payload JSON).", ["external-write", "state-change"], "eventId"),
12406
12479
  "event flow image finalize": payloadCommand((api, input) => api.eventFlow.finalizeImageAsset.mutate(input), "Finalize an event-local registration-flow image after the external upload (--payload JSON).", ["state-change"], "assetId"),
12407
12480
  "event logo upload create": payloadCommand((api, input) => api.event.createLogoUpload.mutate(input), "Create an event-logo upload URL (--payload JSON).", ["external-write", "state-change"], "eventId"),
12408
12481
  "event logo update": payloadCommand((api, input) => api.event.updateLogo.mutate(input), "Set or clear the event logo (--payload JSON).", ["state-change"], "eventId"),
@@ -12541,7 +12614,7 @@ Object.assign(commands, {
12541
12614
  "--expected-version": { key: "expectedVersion", type: "number" }
12542
12615
  },
12543
12616
  operation: (api, input) => api.event.getSeatingDraftPreview.query(getSeatingDraftPreviewInputSchema.parse(input)),
12544
- summary: "Check a saved draft against fresh live seats, grants and notification policy; --expected-version is required."
12617
+ summary: "Check fresh seats, grants and notification policy. Returns noticeRecipients: one eligible/ineligible row per affected registration with grant-scoped names, never email addresses; --expected-version is required."
12545
12618
  },
12546
12619
  "event seating draft publish": {
12547
12620
  scope: "event",
@@ -12557,7 +12630,7 @@ Object.assign(commands, {
12557
12630
  },
12558
12631
  validateInput: (input) => validateSeatingDraftInput(publishSeatingDraftInputSchema, input),
12559
12632
  operation: (api, input) => api.event.publishSeatingDraft.mutate(publishSeatingDraftInputSchema.parse(input)),
12560
- summary: "Atomically publish all draft changes with expectedVersion, UUID requestId and reason. Requires --allow-email: event policy can automatically send notices even when sendSeatChangeEmail is false. Retries reuse requestId and intent. Realtime-enabled free targets need per-registration seatHolds (--payload JSON)."
12633
+ summary: "Atomically publish with expectedVersion, UUID requestId and reason. Optional seatChangeEmailRegistrationIds in --payload chooses exact affected eligible registration recipients; [] sends none, including automatic policy. Omit to retain sendSeatChangeEmail/event-policy behavior. Requires --allow-email. Retry the same requestId and exact recipient set. Realtime free targets need per-registration seatHolds."
12561
12634
  }
12562
12635
  });
12563
12636
  var getCommandManifest = () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/rsvp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "RSVP CLI for authorized event operations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",