@itm-studio/partner-sdk 0.2.9 → 0.3.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.
package/README.md CHANGED
@@ -103,6 +103,71 @@ export function createITMClientForRequest(request: Request) {
103
103
 
104
104
  If you also send `input.compliance`, the backend keeps the values you provide and backfills missing `ipAddress` and `userAgent` from the forwarded headers when available.
105
105
 
106
+ ### Brand Agent session authentication (first-party only)
107
+
108
+ First-party ITM agents authenticate with a short-lived Brand Agent session
109
+ instead of a partner API token. This mode is not available to external
110
+ integrators: it requires workload identity (e.g. a Vercel OIDC token) trusted
111
+ by the ITM backend and a backend-registered agent session, and only the
112
+ capability-scoped operations granted to the session (`CREATE_MOMENT`,
113
+ `SEND_EMAIL`, `READ_MOMENT_ANALYTICS`) will authorize. Everything else stays
114
+ partner-token-only, and a request carrying both a partner API key and an
115
+ agent session is rejected as ambiguous.
116
+
117
+ ```typescript
118
+ import { createITMPartnerClient } from "@itm-studio/partner-sdk";
119
+
120
+ const itm = createITMPartnerClient({
121
+ agentSession: {
122
+ // Minted fresh for every request; never cached or persisted.
123
+ workloadToken: () => getWorkloadOidcToken(),
124
+ sessionUid,
125
+ bootstrapToken, // optional one-time session bootstrap
126
+ },
127
+ });
128
+ ```
129
+
130
+ The brand is always derived server-side from the session — this mode never
131
+ carries a brand identifier or a partner API key. Agent-session mutations must
132
+ supply `toolCallId` on their inputs, plus `idempotencyKey` where it is not
133
+ already required (`CreatePartnerMomentInput`; `SendPartnerAgentEmailInput`
134
+ requires `idempotencyKey` for every caller). Partner-token callers must omit
135
+ the agent-only fields (`toolCallId`, and `idempotencyKey` on
136
+ `CreatePartnerMomentInput`).
137
+
138
+ ### Brand Agent access-token authentication (first-party only)
139
+
140
+ A third mode exists for the one moment in the session lifecycle when no agent
141
+ session exists yet: fetching a conversation thread's seed context while a new
142
+ session is being created. It authenticates with workload identity plus the
143
+ short-lived agent access token the caller presented, and it authorizes only
144
+ the operations guarded for it (currently `getAgentThreadSeed`, which takes no
145
+ arguments — the admin, brand, and thread scope all come from the token's
146
+ verified claims).
147
+
148
+ ```typescript
149
+ const itm = createITMPartnerClient({
150
+ agentAccess: {
151
+ // Minted fresh for every request; never cached or persisted.
152
+ workloadToken: () => getWorkloadOidcToken(),
153
+ // The raw bearer from the inbound session-create request.
154
+ accessToken,
155
+ },
156
+ });
157
+
158
+ const { getAgentThreadSeed } = await itm.query({
159
+ getAgentThreadSeed: {
160
+ seed: true,
161
+ includedMessages: true,
162
+ totalMessages: true,
163
+ truncated: true,
164
+ },
165
+ });
166
+ ```
167
+
168
+ Exactly one of `token`, `agentSession`, or `agentAccess` must be provided;
169
+ combining modes is rejected.
170
+
106
171
  ## Money Amounts
107
172
 
108
173
  Dynamic-pricing price values use the currency's integer minor unit: cents for
@@ -236,6 +301,52 @@ basis points (`6000` = 60%). Refresh after `validUntil`. Because price is stored
236
301
  per tier, every upcoming admission window for a managed tier must fall within
237
302
  the configured horizon before that tier is eligible.
238
303
 
304
+ ## AgentMail
305
+
306
+ Queue one to ten personalized plain-text emails from the authenticated brand's
307
+ dedicated AgentMail inbox. Each item is a separate single-recipient message.
308
+ The mutation returns after the durable batch is accepted; query the batch for
309
+ final per-message provider status.
310
+
311
+ ```ts
312
+ const queued = await client.mutation({
313
+ sendPartnerAgentEmail: {
314
+ __args: {
315
+ input: {
316
+ idempotencyKey: "venue-outreach-2026-08-13",
317
+ messages: [
318
+ {
319
+ to: "bookings@venue.example",
320
+ subject: "October show inquiry",
321
+ text: "Would October 10 work for a 200-person show?",
322
+ },
323
+ ],
324
+ },
325
+ },
326
+ uid: true,
327
+ status: true,
328
+ acceptedCount: true,
329
+ messages: { uid: true, recipient: true, status: true },
330
+ },
331
+ });
332
+
333
+ const status = await client.query({
334
+ getPartnerAgentEmailBatch: {
335
+ __args: { batchUid: queued.sendPartnerAgentEmail.uid },
336
+ status: true,
337
+ messages: {
338
+ recipient: true,
339
+ status: true,
340
+ agentMailMessageId: true,
341
+ lastErrorCode: true,
342
+ },
343
+ },
344
+ });
345
+ ```
346
+
347
+ Reuse an idempotency key only for the exact same logical batch. `SENT` means
348
+ AgentMail accepted the message; it does not prove inbox delivery.
349
+
239
350
  ## Available Queries
240
351
 
241
352
  ### `getPartnerBrand`
@@ -277,7 +388,10 @@ const { getPartnerMomentsForBrand } = await itm.query({
277
388
  take: 20, // items per page (default: 50, omit to use default)
278
389
  cursor: nextCursor, // from previous response
279
390
  status: "UPCOMING", // UPCOMING | LIVE | ENDED
280
- filters: { includePrivate: true }, // include private moments owned by your brand
391
+ filters: {
392
+ search: "summer launch", // case-insensitive name or slug match
393
+ includePrivate: true, // include private moments owned by your brand
394
+ },
281
395
  sortOrder: "ASC", // ASC | DESC by start date (default: DESC)
282
396
  },
283
397
  moments: { uid: true, name: true, startDate: true },
@@ -292,6 +406,9 @@ By default, only public moments are returned. Set
292
406
  `filters.includePrivate` to `true` to return both public and private moments
293
407
  owned by the brand associated with the partner token. Omitting `filters`, or
294
408
  setting `includePrivate` to `false`, preserves the public-only behavior.
409
+ `filters.search` matches moment names and slugs case-insensitively, is limited
410
+ to 100 characters, and composes with visibility, status, sorting, and cursor
411
+ pagination.
295
412
 
296
413
  ### `getPartnerDrops`
297
414
 
@@ -447,9 +564,13 @@ const { getMomentLineupByToken } = await itm.query({
447
564
 
448
565
  ### `getPartnerMomentStats`
449
566
 
450
- Get cached partner-facing counts for a moment owned by the authenticated partner's brand.
567
+ Get cached partner-facing counts for a moment owned by the authenticated partner's brand. Counts
568
+ can be up to five minutes old.
451
569
 
452
570
  - `ticketsCount` counts confirmed tickets only.
571
+ - `generatedAt` identifies when this cached count set was computed.
572
+ - `paidTicketsCount` counts confirmed tickets with a materialized price above zero.
573
+ - `freeTicketsCount` counts confirmed tickets with a zero materialized price.
453
574
  - `waitlistCount` counts active waitlist entries.
454
575
  - `externalRsvpCount` counts external RSVPs.
455
576
  - `ticketRsvpRequestsCount` counts ticket RSVP requests.
@@ -458,7 +579,10 @@ Get cached partner-facing counts for a moment owned by the authenticated partner
458
579
  const { getPartnerMomentStats } = await itm.query({
459
580
  getPartnerMomentStats: {
460
581
  __args: { momentSlug: "my-event-slug" },
582
+ generatedAt: true,
461
583
  ticketsCount: true,
584
+ paidTicketsCount: true,
585
+ freeTicketsCount: true,
462
586
  waitlistCount: true,
463
587
  externalRsvpCount: true,
464
588
  ticketRsvpRequestsCount: true,
@@ -617,6 +741,55 @@ const { getPartnerCustomersForMoment } = await itm.query({
617
741
  });
618
742
  ```
619
743
 
744
+ ### `getPartnerAudience`
745
+
746
+ Get the authenticated brand's customer audience with explicit filters, one
747
+ deterministic sort, and opaque cursor pagination. Call this only from a trusted
748
+ Node.js server: the partner token and returned customer data must never be
749
+ embedded in a browser/mobile bundle or written to application logs.
750
+
751
+ ```typescript
752
+ const { getPartnerAudience } = await itm.query({
753
+ getPartnerAudience: {
754
+ __args: {
755
+ input: {
756
+ filters: {
757
+ search: "john@example.com",
758
+ emailConsentStatuses: ["SUBSCRIBED"],
759
+ momentUids: ["moment-uid"],
760
+ minTotalSpendUsdCents: 5000,
761
+ },
762
+ sort: { field: "TOTAL_SPEND_USD", direction: "DESC" },
763
+ take: 50, // maximum 100
764
+ includeTotalCount: true, // omitted by default to avoid the count query
765
+ },
766
+ },
767
+ customers: {
768
+ uid: true, // brand-scoped customer UID
769
+ name: true,
770
+ email: true,
771
+ phoneNumber: true,
772
+ emailConsentStatus: true,
773
+ smsConsentStatus: true,
774
+ communicationsOptedOut: true,
775
+ isSuppressed: true,
776
+ totalSpendUsdCents: true,
777
+ numMoments: true,
778
+ updatedAt: true,
779
+ },
780
+ totalCount: true,
781
+ hasNextPage: true,
782
+ nextCursor: true,
783
+ },
784
+ });
785
+ ```
786
+
787
+ Filters are combined with AND; values inside one list are combined with OR.
788
+ `from` timestamps are inclusive and `before` timestamps are exclusive. Audience
789
+ pages and optional exact counts can be up to 30 seconds old. Consent and
790
+ suppression fields reflect ITM's current brand-scoped records but do not replace
791
+ your legal-purpose and communications-compliance checks.
792
+
620
793
  ### `getPartnerMomentCollection`
621
794
 
622
795
  Get a moment collection by slug. Collections group related moments together (e.g. a summer series, a festival lineup). Returns the collection with its moments and links.