@itm-studio/partner-sdk 0.4.0 → 0.4.2

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
@@ -2165,6 +2165,326 @@ console.log(
2165
2165
  );
2166
2166
  ```
2167
2167
 
2168
+ ## Login with ITM
2169
+
2170
+ Let people sign in to your site with their ITM account. Every sign-in also adds the person to your brand's contacts (source `LOGIN_WITH_ITM`), and both ways of signing in identify the person by the same **customer UID** that `getPartnerAudience` returns. A customer UID is stable for your brand and different for every brand.
2171
+
2172
+ There are two ways to run it:
2173
+
2174
+ | | Hosted sign-in (OpenID Connect) | Custom sign-in form |
2175
+ | ------------------------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
2176
+ | Who renders the sign-in screens | ITM, on the app's `issuer` domain, branded with your brand | Your site |
2177
+ | How it runs | Browser redirect to ITM, then a standard authorization-code exchange | `startPartnerAppLogin` and `verifyPartnerAppLogin`, from your server |
2178
+ | What you receive | An ID token and an access token issued by ITM (1 hour, no refresh) | `customerUid` and a profile. **No token**: you keep your own session |
2179
+ | Partner key used | Only to register the app | On every call |
2180
+ | Marketing opt-in | One optional checkbox on ITM's code and "Continue" screens | One optional checkbox, recorded with the sign-in |
2181
+ | Person identifier | The ID token's `sub` claim (equal to the customer UID) | `customerUid` |
2182
+
2183
+ Use the hosted sign-in unless you need the form inside your own UI. Both start with an app.
2184
+
2185
+ ### Register an app
2186
+
2187
+ An app is an OpenID Connect client for one of your sites. There are two kinds:
2188
+
2189
+ - **Browser app:** a public client with no secret. `createPartnerBrandApp` registers this kind, and it is all the custom sign-in form needs.
2190
+ - **Server website:** a confidential client whose server keeps a client secret, so tokens never reach the browser. Create it in Backstage under **Manage › Advanced settings › Login with ITM**. Backstage shows the client secret once and can rotate it.
2191
+
2192
+ Register a Browser app once, from your server:
2193
+
2194
+ ```typescript
2195
+ const { createPartnerBrandApp: app } = await itm.mutation({
2196
+ createPartnerBrandApp: {
2197
+ __args: {
2198
+ input: {
2199
+ name: "My site",
2200
+ redirectUris: ["https://example.com/auth/itm"],
2201
+ },
2202
+ },
2203
+ uid: true,
2204
+ clientId: true,
2205
+ redirectUris: true,
2206
+ issuer: true,
2207
+ status: true,
2208
+ },
2209
+ });
2210
+ ```
2211
+
2212
+ - `name`: 1 to 80 characters.
2213
+ - `redirectUris`: 1 to 10 exact URIs, all on **one origin**, using `https` (`http` is allowed only for `localhost`). No wildcards, fragments or credentials. ITM stores them normalized, so use the strings returned in `app.redirectUris` exactly as your `redirect_uri`. For another site, register another app.
2214
+ - The redirect origin is also the only browser origin allowed to call the issuer's token and userinfo endpoints.
2215
+ - `clientId` starts with `itm_app_`. It is public, not a secret.
2216
+ - `issuer` is the OpenID Connect issuer for the hosted sign-in. It is `null` when hosted sign-in is not configured in that ITM environment; the custom sign-in form still works.
2217
+ - A brand can have up to 20 apps. `getPartnerBrandApps` lists them, oldest first.
2218
+ - `disablePartnerBrandApp(appUid)` stops new sign-ins through both flows immediately and revokes the app's tokens and remembered consents. A partner key cannot re-enable an app; ask ITM.
2219
+
2220
+ Only partner keys can call these five operations. Brand Agent sessions and access tokens cannot.
2221
+
2222
+ ### Hosted sign-in (OpenID Connect)
2223
+
2224
+ Hosted sign-in is standard OpenID Connect, so you do not need this SDK for it, and your partner key is never part of it. Any certified OpenID Connect client library works. The facts a library needs:
2225
+
2226
+ - **Discovery:** `${issuer}/.well-known/openid-configuration`. Read the endpoints from it rather than hard-coding paths.
2227
+ - **Flow:** authorization code with PKCE. `response_type=code` and `code_challenge_method=S256` are the only values accepted, and PKCE is required.
2228
+ - **Client authentication:** a Browser app is a public client with no secret (`token_endpoint_auth_method: none`). A Server website app authenticates with its client secret using `client_secret_basic`.
2229
+ - **Scopes:** `openid profile email phone`. Every app gets this fixed set.
2230
+ - **ID token:** verify it against the issuer's `jwks_uri`, with `aud` equal to your client ID and the `nonce` you sent. Its `sub` is pairwise: it equals the person's customer UID for your brand.
2231
+ - **Profile claims:** read them from the userinfo endpoint with the access token. It returns `name`, `email`, `email_verified`, `phone_number` and `phone_number_verified`, and leaves out any the person does not have. The access token is opaque.
2232
+ - **Lifetimes:** authorization codes last 60 seconds; access and ID tokens last 1 hour. There are no refresh tokens (`offline_access` is not supported). When you need new tokens, send the person through the flow again. While their ITM session on the issuer lasts (up to 30 days), a person who already approved your app comes straight back without a prompt.
2233
+ - **Revocation** is available. There is no logout endpoint: end your own session.
2234
+
2235
+ The person sees your brand's name and logo, gets a code by SMS or email, and approves sharing their name, email address and phone number the first time. They become a contact. They are subscribed to your brand's marketing only if they tick the optional, unticked checkbox on ITM's page.
2236
+
2237
+ Run the token exchange on your server and keep tokens out of the browser, following the Backend for Frontend pattern ([RFC 10017 §6.1](https://www.rfc-editor.org/rfc/rfc10017.html)). Prefer an OpenID Connect client library that performs the protocol validation. This framework-neutral sketch with `fetch` and [`jose`](https://github.com/panva/jose) shows the main steps; `rotateSession()` represents your framework's session regeneration API and must invalidate the old session ID and return the new session.
2238
+
2239
+ ```typescript
2240
+ import { createHash, randomBytes } from "node:crypto";
2241
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2242
+
2243
+ if (!app.issuer) throw new Error("Hosted sign-in is not configured");
2244
+ const discoveryResponse = await fetch(`${app.issuer}/.well-known/openid-configuration`);
2245
+ if (!discoveryResponse.ok) throw new Error("Discovery failed");
2246
+ const discovery = await discoveryResponse.json();
2247
+ if (discovery.issuer !== app.issuer) throw new Error("Bad issuer");
2248
+ const redirectUri = app.redirectUris[0];
2249
+
2250
+ // 1. Send the browser to ITM.
2251
+ const login = {
2252
+ verifier: randomBytes(32).toString("base64url"),
2253
+ state: randomBytes(16).toString("base64url"),
2254
+ nonce: randomBytes(16).toString("base64url"),
2255
+ };
2256
+ session.itmLogin = login;
2257
+ const authorize = new URL(discovery.authorization_endpoint);
2258
+ authorize.search = new URLSearchParams({
2259
+ client_id: app.clientId,
2260
+ redirect_uri: redirectUri,
2261
+ response_type: "code",
2262
+ scope: "openid profile email phone",
2263
+ state: login.state,
2264
+ nonce: login.nonce,
2265
+ code_challenge: createHash("sha256").update(login.verifier).digest("base64url"),
2266
+ code_challenge_method: "S256",
2267
+ }).toString();
2268
+ res.redirect(authorize.href);
2269
+
2270
+ // 2. On the redirect URI: check state, exchange the code, verify the ID token.
2271
+ const pending = session.itmLogin;
2272
+ if (!pending || typeof req.query.state !== "string" || req.query.state !== pending.state) {
2273
+ throw new Error("Bad state");
2274
+ }
2275
+ delete session.itmLogin; // consume and persist this transaction once, including on errors
2276
+ if (req.query.error) throw new Error("Sign-in was not completed");
2277
+ if (typeof req.query.code !== "string" || !req.query.code) throw new Error("Missing code");
2278
+ const tokenResponse = await fetch(discovery.token_endpoint, {
2279
+ method: "POST",
2280
+ headers: { "content-type": "application/x-www-form-urlencoded" },
2281
+ body: new URLSearchParams({
2282
+ grant_type: "authorization_code",
2283
+ code: req.query.code,
2284
+ redirect_uri: redirectUri,
2285
+ client_id: app.clientId,
2286
+ code_verifier: pending.verifier,
2287
+ }),
2288
+ });
2289
+ if (!tokenResponse.ok) throw new Error("Token exchange failed");
2290
+ const tokens = await tokenResponse.json();
2291
+ const { payload } = await jwtVerify(
2292
+ tokens.id_token,
2293
+ createRemoteJWKSet(new URL(discovery.jwks_uri)),
2294
+ {
2295
+ issuer: app.issuer,
2296
+ audience: app.clientId,
2297
+ algorithms: ["RS256"], // ITM's registered default ID-token signing algorithm
2298
+ requiredClaims: ["sub", "iat", "exp", "nonce"],
2299
+ },
2300
+ );
2301
+ if (payload.nonce !== pending.nonce) throw new Error("Bad nonce");
2302
+ if (typeof payload.sub !== "string" || !payload.sub) throw new Error("Missing subject");
2303
+ if ((Array.isArray(payload.aud) && payload.aud.length > 1 && !payload.azp) ||
2304
+ (payload.azp !== undefined && payload.azp !== app.clientId)) {
2305
+ throw new Error("Bad authorized party");
2306
+ }
2307
+
2308
+ // 3. Optional: read the profile.
2309
+ const profileResponse = await fetch(discovery.userinfo_endpoint, {
2310
+ headers: { authorization: `Bearer ${tokens.access_token}` },
2311
+ });
2312
+ if (!profileResponse.ok) throw new Error("Profile request failed");
2313
+ const profile = await profileResponse.json();
2314
+ if (profile.sub !== payload.sub) throw new Error("Profile subject mismatch");
2315
+
2316
+ // Only establish the authenticated session after all checks succeed.
2317
+ const authenticatedSession = await rotateSession();
2318
+ authenticatedSession.customerUid = payload.sub;
2319
+ ```
2320
+
2321
+ The sketch uses a Browser app. With a Server website app, also authenticate the token request with `client_secret_basic` (an `authorization: Basic …` header built from the client ID and client secret) and keep the secret on your server. For a complete server example using `openid-client`, see [`examples/login-with-itm`](https://github.com/itm-studio/demo-events-sdk-integration/tree/main/examples/login-with-itm).
2322
+
2323
+ A redirect that comes back with `error=access_denied` means the person cancelled.
2324
+ Never use UserInfo claims until its `sub` matches the validated ID token's `sub` ([OpenID Connect Core §5.3.2](https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse)). Persist the pending login before redirecting, expire it after a short interval, and consume it once on callback using your session store's concurrency controls.
2325
+
2326
+ ### Custom sign-in form
2327
+
2328
+ Your site renders the sign-in form. ITM sends and checks the code, finds or creates the person's ITM account, and adds them to your brand's contacts, optionally with a marketing opt-in. Both calls run **from your server** with your partner key.
2329
+
2330
+ #### Step 1: send a code
2331
+
2332
+ ```typescript
2333
+ const { startPartnerAppLogin: challenge } = await itm.mutation({
2334
+ startPartnerAppLogin: {
2335
+ __args: {
2336
+ input: {
2337
+ clientId: app.clientId,
2338
+ identifier, // phone number or email address the visitor typed
2339
+ compliance: {
2340
+ ipAddress: visitorIp, // required: the visitor's IP, not your server's
2341
+ userAgent: req.headers["user-agent"],
2342
+ pageUrl: "https://example.com/login",
2343
+ },
2344
+ },
2345
+ },
2346
+ channel: true,
2347
+ destinationHint: true,
2348
+ expiresAt: true,
2349
+ disclosure: true,
2350
+ marketingConsentPrompt: {
2351
+ promptUid: true,
2352
+ labelMarkdown: true,
2353
+ bodyMarkdown: true,
2354
+ channels: true,
2355
+ },
2356
+ },
2357
+ });
2358
+ session.itmLoginIdentifier = identifier; // keep it server-side for step 3
2359
+ ```
2360
+
2361
+ - `identifier`: an email address, or a phone number in E.164 (`+14155550142`). A number without a country code is read as a US number.
2362
+ - `compliance.ipAddress` is required on both steps. Unlike partner subscriptions, sign-in does **not** fill it in from forwarded headers.
2363
+ - `channel` is `EMAIL` for email addresses. For phone numbers it is usually `SMS`, but ITM's delivery checks can send the code by `WHATSAPP` or a voice `CALL` instead. Tell the visitor where to look.
2364
+ - `destinationHint` is the masked phone number or email address to show, and `expiresAt` is when the code stops working (10 minutes).
2365
+ - The response is the same whether or not the person already has an ITM account, so it cannot be used to discover accounts.
2366
+
2367
+ #### Step 2: show the code field
2368
+
2369
+ Render on the same screen:
2370
+
2371
+ - `challenge.disclosure`, verbatim. It tells the visitor what signing in shares with you.
2372
+ - `challenge.marketingConsentPrompt` as **one optional, unchecked** checkbox: `labelMarkdown` as its label and `bodyMarkdown` below it. Signing in must not depend on it. It covers SMS and email marketing from your brand via ITM.
2373
+
2374
+ #### Step 3: verify
2375
+
2376
+ ```typescript
2377
+ const { verifyPartnerAppLogin: login } = await itm.mutation({
2378
+ verifyPartnerAppLogin: {
2379
+ __args: {
2380
+ input: {
2381
+ clientId: app.clientId,
2382
+ identifier: session.itmLoginIdentifier, // never from the browser
2383
+ code, // 6 digits; spaces are ignored
2384
+ compliance: { ipAddress: visitorIp, userAgent: req.headers["user-agent"] },
2385
+ // Only when the box was ticked; omitting it never unsubscribes anyone.
2386
+ // pageUrl is required with an opt-in (here or in compliance).
2387
+ ...(optedIn
2388
+ ? { marketingConsent: { accepted: true, pageUrl: "https://example.com/login" } }
2389
+ : {}),
2390
+ },
2391
+ },
2392
+ customerUid: true,
2393
+ profile: {
2394
+ name: true,
2395
+ email: true,
2396
+ emailVerified: true,
2397
+ phoneNumber: true,
2398
+ phoneVerified: true,
2399
+ },
2400
+ },
2401
+ });
2402
+ delete session.itmLoginIdentifier;
2403
+ const authenticatedSession = await rotateSession(); // invalidate the old session ID
2404
+ authenticatedSession.customerUid = login.customerUid;
2405
+ ```
2406
+
2407
+ The response is **not a credential**. ITM issues no token in this flow: keep your own session, keyed by `customerUid`. The same person signing in again gets the same `customerUid`; `profile` reflects their ITM account at that moment, and fields they have not provided are `null`.
2408
+
2409
+ #### Errors
2410
+
2411
+ Every failure throws a `GenqlError`. Its `errors[0].message` is the text below and `errors[0].extensions.code` is the code:
2412
+
2413
+ | Situation | Operation | `code` | `message` |
2414
+ | --- | --- | --- | --- |
2415
+ | No partner key, or an invalid one | all | `UNAUTHENTICATED` | `No partner access token provided…` or an invalid-token message |
2416
+ | `clientId` is unknown, belongs to another brand, or the app is disabled | start, verify | `NOT_FOUND` | `App not found` |
2417
+ | `appUid` is unknown or belongs to another brand | disable | `NOT_FOUND` | `App not found` |
2418
+ | `identifier` is neither a phone number nor an email address | start, verify | `BAD_USER_INPUT` | `Enter a valid phone number or email address` |
2419
+ | `compliance.ipAddress` is missing or not an IP address | start, verify | `BAD_USER_INPUT` | `compliance.ipAddress must be the visitor's IP address` |
2420
+ | A code was sent to this person for this app less than a minute ago. The earlier code still works | start | `RATE_LIMITED` | `A code was already requested. Wait a minute before requesting another.` |
2421
+ | Too many codes sent to this person, or by your brand today | start | `RATE_LIMITED` | `Too many attempts. Wait a few minutes and try again.` |
2422
+ | Too many codes sent to this email address or from this IP | start | `BAD_USER_INPUT` | `Too many verification code requests` |
2423
+ | ITM does not send codes to this phone number | start | `BAD_USER_INPUT` | `Verification is not available for this phone number.` |
2424
+ | ITM's SMS fraud limits for this number or IP, or a temporary SMS outage | start, verify (phone) | `BAD_USER_INPUT` | `Too many verification requests. Please wait before trying again.` or `Verification is temporarily unavailable. Please try again shortly.` |
2425
+ | Wrong code, or a code requested with a different partner key | verify | `BAD_USER_INPUT` | `Invalid verification code` |
2426
+ | The code expired, was already used, or was never sent | verify | `BAD_USER_INPUT` | `Invalid or expired verification code` |
2427
+ | Fifth wrong code. The code is discarded; send a new one | verify | `BAD_USER_INPUT` | `Too many attempts. Request a new code.` |
2428
+ | Too many verification attempts for this person | verify | `RATE_LIMITED` | `Too many attempts. Wait a few minutes and try again.` |
2429
+ | `marketingConsent.accepted` without a `pageUrl`. The code is not used up | verify | `BAD_USER_INPUT` | `pageUrl is required when the visitor opts in` |
2430
+ | The account cannot sign in (for example an imported contact that never signed up). Nothing is recorded | verify | `BAD_USER_INPUT` | `This account can't be used to log in here. Contact ITM support.` |
2431
+ | More requests per minute than your key's limit | all | `RATE_LIMITED` | varies |
2432
+
2433
+ ```typescript
2434
+ import { GenqlError } from "@itm-studio/partner-sdk";
2435
+
2436
+ try {
2437
+ await itm.mutation({ verifyPartnerAppLogin: { /* … */ } });
2438
+ } catch (error) {
2439
+ if (error instanceof GenqlError) {
2440
+ const [first] = error.errors;
2441
+ // Show first.message to the visitor; branch on first.extensions?.code.
2442
+ }
2443
+ throw error;
2444
+ }
2445
+ ```
2446
+
2447
+ #### Limits
2448
+
2449
+ | Limit | Value |
2450
+ | --- | --- |
2451
+ | Code lifetime | 10 minutes |
2452
+ | New code for the same person and app | after 1 minute |
2453
+ | Wrong codes per code | 5 |
2454
+ | Codes sent to one phone number or email address | 5 per 10 minutes, shared with hosted sign-in |
2455
+ | Codes sent by one brand | 2,000 per 24 hours |
2456
+ | Email codes | 5 per hour and 10 per day per address; 30 per hour per visitor IP |
2457
+ | Verification attempts per person and app | 10 per 15 minutes |
2458
+ | `startPartnerAppLogin`, `verifyPartnerAppLogin` | 30 requests per minute per partner key |
2459
+ | `createPartnerBrandApp`, `disablePartnerBrandApp` | 20 requests per minute per partner key |
2460
+ | `getPartnerBrandApps` | 120 requests per minute per partner key |
2461
+
2462
+ ### Marketing opt-in at sign-in
2463
+
2464
+ When the visitor ticks the checkbox from step 2 and you send `marketingConsent: { accepted: true }`:
2465
+
2466
+ - ITM stores the opt-in as consent evidence: the prompt text it supplied, the time, the visitor's IP address and user agent, and the page URL.
2467
+ - Shortly after the sign-in completes, ITM subscribes the person to your brand for SMS and email marketing. This runs asynchronously, so it is not reflected in the `verifyPartnerAppLogin` response.
2468
+ - Each opt-in is recorded as new evidence, including repeat opt-ins. Leaving the box unticked records nothing and never unsubscribes anyone.
2469
+ - People your brand has blacklisted can still sign in, but an opt-in never subscribes them.
2470
+
2471
+ Hosted sign-in offers the same optional checkbox on ITM's code and "Continue to {Brand}" screens, and ITM records a ticked box as an opt-in. To subscribe someone outside sign-in, collect consent on your own page and call `createPartnerBrandSubscription`; see the [Partner Brand Subscription Guide](#partner-brand-subscription-guide).
2472
+
2473
+ ### What ITM handles
2474
+
2475
+ - Codes only work for the app and the partner key that requested them. They cannot sign anyone in to ITM itself or to another app.
2476
+ - New and returning people get identical responses until the code is verified.
2477
+ - The person becomes a contact and is **not** subscribed to marketing unless they opted in.
2478
+
2479
+ ### What your site must do
2480
+
2481
+ - Keep the partner key and both custom-form calls on your server, and keep hosted sign-in tokens there too.
2482
+ - Read `identifier` for step 3 from your own session, not from the browser.
2483
+ - Pass the visitor's real IP address on both steps of the custom form.
2484
+ - Store your session in `Secure`, `HttpOnly`, `SameSite` cookies, as in the Backend for Frontend pattern ([RFC 10017 §6.1](https://www.rfc-editor.org/rfc/rfc10017.html)).
2485
+ - Regenerate the session ID after either sign-in flow succeeds, invalidate the old session, and clear pending login state. Use your framework's session API for the `rotateSession()` placeholder above.
2486
+ - Protect your send-code and verify endpoints with CSRF tokens and your own bot protection.
2487
+
2168
2488
  ## Pagination
2169
2489
 
2170
2490
  All list endpoints use cursor-based pagination. The default page size is **50** items if `take` is not specified.
@@ -2198,7 +2518,7 @@ For staging or local development:
2198
2518
  ```typescript
2199
2519
  const itm = createITMPartnerClient({
2200
2520
  token: process.env.ITM_PARTNER_TOKEN!,
2201
- baseUrl: "https://staging-api.itm.studio/graphql",
2521
+ baseUrl: "https://staging.itm-api.com/graphql",
2202
2522
  });
2203
2523
  ```
2204
2524
 
package/dist/index.d.mts CHANGED
@@ -902,7 +902,63 @@ interface PartnerMembershipTier {
902
902
  yearlyPrice: (Scalars['Int'] | null);
903
903
  __typename: 'PartnerMembershipTier';
904
904
  }
905
+ /** Whether a Login with ITM app can sign users in. */
906
+ type BrandAppStatus = 'ACTIVE' | 'DISABLED';
907
+ type ConsentChannel = 'EMAIL' | 'PUSH' | 'SMS';
908
+ type ConsentPromptCategory = 'CUSTOM' | 'MARKETING' | 'PRIVACY' | 'TERMS';
909
+ type LoginCodeChannel = 'CALL' | 'EMAIL' | 'SMS' | 'WHATSAPP';
910
+ /** A brand-owned Login with ITM application: a public OAuth/OIDC client that uses PKCE and has no secret. */
911
+ interface BrandApp {
912
+ /** Public OAuth client_id. It is not a secret. */
913
+ clientId: Scalars['String'];
914
+ createdAt: Scalars['DateTime'];
915
+ /** OpenID Connect issuer URL apps authenticate against; null when Login with ITM is not configured. */
916
+ issuer: (Scalars['String'] | null);
917
+ name: Scalars['String'];
918
+ /** Exact redirect URIs. All share one origin; that origin may call the token and userinfo endpoints and hand logins to ITM embeds. */
919
+ redirectUris: Scalars['String'][];
920
+ status: BrandAppStatus;
921
+ uid: Scalars['String'];
922
+ __typename: 'BrandApp';
923
+ }
924
+ interface CheckoutMarketingConsentPrompt {
925
+ bodyMarkdown: (Scalars['String'] | null);
926
+ category: ConsentPromptCategory;
927
+ channels: ConsentChannel[];
928
+ labelMarkdown: Scalars['String'];
929
+ promptUid: Scalars['String'];
930
+ required: Scalars['Boolean'];
931
+ version: Scalars['Int'];
932
+ __typename: 'CheckoutMarketingConsentPrompt';
933
+ }
934
+ interface PartnerAppLoginChallenge {
935
+ channel: LoginCodeChannel;
936
+ destinationHint: (Scalars['String'] | null);
937
+ /** Show this text verbatim with the code field: it is what signing in shares. */
938
+ disclosure: Scalars['String'];
939
+ expiresAt: Scalars['DateTime'];
940
+ /** Optional marketing opt-in. Render verbatim as an unchecked checkbox; signing in must not depend on it. */
941
+ marketingConsentPrompt: CheckoutMarketingConsentPrompt;
942
+ __typename: 'PartnerAppLoginChallenge';
943
+ }
944
+ interface PartnerAppLoginProfile {
945
+ email: (Scalars['String'] | null);
946
+ emailVerified: Scalars['Boolean'];
947
+ name: (Scalars['String'] | null);
948
+ phoneNumber: (Scalars['String'] | null);
949
+ phoneVerified: Scalars['Boolean'];
950
+ __typename: 'PartnerAppLoginProfile';
951
+ }
952
+ /** A completed headless sign-in. It is not a credential: keep your own session keyed by customerUid. */
953
+ interface PartnerAppLogin {
954
+ /** The person's customer UID for your brand (getPartnerAudience customer uid). */
955
+ customerUid: Scalars['String'];
956
+ profile: PartnerAppLoginProfile;
957
+ __typename: 'PartnerAppLogin';
958
+ }
905
959
  interface Query {
960
+ /** The brand's Login with ITM apps (at most 20), oldest first. Deleted apps are omitted. */
961
+ getPartnerBrandApps: BrandApp[];
906
962
  /** List the authenticated partner brand's membership rows; PENDING is excluded by default. */
907
963
  getPartnerMembers: GetPartnerMembersResponse;
908
964
  /** List the authenticated partner brand's non-deleted membership tiers, including inactive and private tiers. */
@@ -1057,6 +1113,14 @@ interface CreatePartnerBrandSubscriptionResponse {
1057
1113
  __typename: 'CreatePartnerBrandSubscriptionResponse';
1058
1114
  }
1059
1115
  interface Mutation {
1116
+ /** Registers a Login with ITM app for the brand. A brand can have up to 20 apps. */
1117
+ createPartnerBrandApp: BrandApp;
1118
+ /** Disables an app: new logins stop and its tokens and remembered consents are revoked immediately. A partner key cannot re-enable it; ask ITM. */
1119
+ disablePartnerBrandApp: BrandApp;
1120
+ /** Sends a Login with ITM code for the brand's own sign-in form. Call it from your server; the response is the same whether or not the person has an ITM account. */
1121
+ startPartnerAppLogin: PartnerAppLoginChallenge;
1122
+ /** Completes a headless sign-in: checks the code, adds the person to the brand's contacts and records an optional opt-in. Returns no ITM credential. */
1123
+ verifyPartnerAppLogin: PartnerAppLogin;
1060
1124
  /** Log a payment received off-site. Brand scope comes from the authenticated token or agent session. Amount is in ITM cents (major units times 100). Retain requestUid across retries. */
1061
1125
  createPartnerManualContribution: ContributionAdmin;
1062
1126
  reserveAgentMediaUpload: AgentMediaUploadReservation;
@@ -2319,7 +2383,97 @@ interface PartnerMembershipTierGenqlSelection {
2319
2383
  __typename?: boolean | number;
2320
2384
  __scalar?: boolean | number;
2321
2385
  }
2386
+ /** A brand-owned Login with ITM application: a public OAuth/OIDC client that uses PKCE and has no secret. */
2387
+ interface BrandAppGenqlSelection {
2388
+ /** Public OAuth client_id. It is not a secret. */
2389
+ clientId?: boolean | number;
2390
+ createdAt?: boolean | number;
2391
+ /** OpenID Connect issuer URL apps authenticate against; null when Login with ITM is not configured. */
2392
+ issuer?: boolean | number;
2393
+ name?: boolean | number;
2394
+ /** Exact redirect URIs. All share one origin; that origin may call the token and userinfo endpoints and hand logins to ITM embeds. */
2395
+ redirectUris?: boolean | number;
2396
+ status?: boolean | number;
2397
+ uid?: boolean | number;
2398
+ __typename?: boolean | number;
2399
+ __scalar?: boolean | number;
2400
+ }
2401
+ interface CreateBrandAppInput {
2402
+ name: Scalars['String'];
2403
+ /** Exact https redirect URIs (http is allowed only for localhost) on a single origin. */
2404
+ redirectUris: Scalars['String'][];
2405
+ }
2406
+ interface CheckoutMarketingConsentPromptGenqlSelection {
2407
+ bodyMarkdown?: boolean | number;
2408
+ category?: boolean | number;
2409
+ channels?: boolean | number;
2410
+ labelMarkdown?: boolean | number;
2411
+ promptUid?: boolean | number;
2412
+ required?: boolean | number;
2413
+ version?: boolean | number;
2414
+ __typename?: boolean | number;
2415
+ __scalar?: boolean | number;
2416
+ }
2417
+ interface CheckoutMarketingConsentInput {
2418
+ accepted: Scalars['Boolean'];
2419
+ language?: (Scalars['String'] | null);
2420
+ pageUrl?: (Scalars['String'] | null);
2421
+ referrerUrl?: (Scalars['String'] | null);
2422
+ screenHeight?: (Scalars['Int'] | null);
2423
+ screenWidth?: (Scalars['Int'] | null);
2424
+ submittedAt?: (Scalars['DateTime'] | null);
2425
+ timezone?: (Scalars['String'] | null);
2426
+ }
2427
+ /** Starts a headless Login with ITM sign-in from the brand's server: ITM sends a code that only verifyPartnerAppLogin accepts for this app and partner credential. */
2428
+ interface StartPartnerAppLoginInput {
2429
+ /** The app's public client ID. */
2430
+ clientId: Scalars['String'];
2431
+ /** The visitor's request details. ipAddress (the visitor's, not your server's) is required. */
2432
+ compliance: SubscriptionComplianceInput;
2433
+ /** Phone number or email address. */
2434
+ identifier: Scalars['String'];
2435
+ }
2436
+ interface PartnerAppLoginChallengeGenqlSelection {
2437
+ channel?: boolean | number;
2438
+ destinationHint?: boolean | number;
2439
+ /** Show this text verbatim with the code field: it is what signing in shares. */
2440
+ disclosure?: boolean | number;
2441
+ expiresAt?: boolean | number;
2442
+ /** Optional marketing opt-in. Render verbatim as an unchecked checkbox; signing in must not depend on it. */
2443
+ marketingConsentPrompt?: CheckoutMarketingConsentPromptGenqlSelection;
2444
+ __typename?: boolean | number;
2445
+ __scalar?: boolean | number;
2446
+ }
2447
+ interface VerifyPartnerAppLoginInput {
2448
+ clientId: Scalars['String'];
2449
+ code: Scalars['String'];
2450
+ /** The visitor's request details. ipAddress is required. */
2451
+ compliance: SubscriptionComplianceInput;
2452
+ /** The identifier the code was sent to. Read it from your own session, not from the browser. */
2453
+ identifier: Scalars['String'];
2454
+ /** Send only when the visitor ticked the opt-in (accepted: true; pageUrl here or in compliance). Omit it otherwise; omission never unsubscribes anyone. */
2455
+ marketingConsent?: (CheckoutMarketingConsentInput | null);
2456
+ }
2457
+ interface PartnerAppLoginProfileGenqlSelection {
2458
+ email?: boolean | number;
2459
+ emailVerified?: boolean | number;
2460
+ name?: boolean | number;
2461
+ phoneNumber?: boolean | number;
2462
+ phoneVerified?: boolean | number;
2463
+ __typename?: boolean | number;
2464
+ __scalar?: boolean | number;
2465
+ }
2466
+ /** A completed headless sign-in. It is not a credential: keep your own session keyed by customerUid. */
2467
+ interface PartnerAppLoginGenqlSelection {
2468
+ /** The person's customer UID for your brand (getPartnerAudience customer uid). */
2469
+ customerUid?: boolean | number;
2470
+ profile?: PartnerAppLoginProfileGenqlSelection;
2471
+ __typename?: boolean | number;
2472
+ __scalar?: boolean | number;
2473
+ }
2322
2474
  interface QueryGenqlSelection {
2475
+ /** The brand's Login with ITM apps (at most 20), oldest first. Deleted apps are omitted. */
2476
+ getPartnerBrandApps?: BrandAppGenqlSelection;
2323
2477
  /** List the authenticated partner brand's membership rows; PENDING is excluded by default. */
2324
2478
  getPartnerMembers?: (GetPartnerMembersResponseGenqlSelection & {
2325
2479
  __args?: {
@@ -2914,6 +3068,30 @@ interface CreatePartnerBrandSubscriptionResponseGenqlSelection {
2914
3068
  __scalar?: boolean | number;
2915
3069
  }
2916
3070
  interface MutationGenqlSelection {
3071
+ /** Registers a Login with ITM app for the brand. A brand can have up to 20 apps. */
3072
+ createPartnerBrandApp?: (BrandAppGenqlSelection & {
3073
+ __args: {
3074
+ input: CreateBrandAppInput;
3075
+ };
3076
+ });
3077
+ /** Disables an app: new logins stop and its tokens and remembered consents are revoked immediately. A partner key cannot re-enable it; ask ITM. */
3078
+ disablePartnerBrandApp?: (BrandAppGenqlSelection & {
3079
+ __args: {
3080
+ appUid: Scalars['String'];
3081
+ };
3082
+ });
3083
+ /** Sends a Login with ITM code for the brand's own sign-in form. Call it from your server; the response is the same whether or not the person has an ITM account. */
3084
+ startPartnerAppLogin?: (PartnerAppLoginChallengeGenqlSelection & {
3085
+ __args: {
3086
+ input: StartPartnerAppLoginInput;
3087
+ };
3088
+ });
3089
+ /** Completes a headless sign-in: checks the code, adds the person to the brand's contacts and records an optional opt-in. Returns no ITM credential. */
3090
+ verifyPartnerAppLogin?: (PartnerAppLoginGenqlSelection & {
3091
+ __args: {
3092
+ input: VerifyPartnerAppLoginInput;
3093
+ };
3094
+ });
2917
3095
  /** Log a payment received off-site. Brand scope comes from the authenticated token or agent session. Amount is in ITM cents (major units times 100). Retain requestUid across retries. */
2918
3096
  createPartnerManualContribution?: (ContributionAdminGenqlSelection & {
2919
3097
  __args: {
@@ -3553,4 +3731,4 @@ type ITMPartnerClient = Client;
3553
3731
  */
3554
3732
  declare function createITMPartnerClient(config: ITMPartnerClientConfig): ITMPartnerClient;
3555
3733
 
3556
- export { type AgentThreadSeedContext, type ApplyDynamicPricingInput, type BrandAgentEmailMessageInput, type BrandAgentEmailSendBatch, type BrandAgentEmailSendBatchStatus, type BrandAgentEmailSendMessage, type BrandAgentEmailSendMessageStatus, type BrandDrop, type BrandDropCtaType, type BrandDropMusicLink, type BrandSubscription, type ConsentStatus, type ContributionAdmin, type ContributionPageAdmin, type ContributionPageStatusFilter, type ContributionPagesResponse, type ContributionSource, type ContributionStatus, type ContributionsResponse, type CreateManualContributionInput, type CreateMomentOnlineConfigInput, type CreatePartnerBrandDropInput, type CreatePartnerBrandSubscriptionInput, type CreatePartnerBrandSubscriptionResponse, type CreatePartnerMomentInput, type CreatePartnerTicketTierInput, type CreatePlanBudgetInput, type CreatePlanInput, type Currency, DEFAULT_API_URL, type Dimensions, type DimensionsInput, type DynamicPricingActorType, type DynamicPricingAdmissionWindow, type DynamicPricingBatch, type DynamicPricingBatchEvent, type DynamicPricingBatchEventType, type DynamicPricingBatchItem, type DynamicPricingBatchItemStatus, type DynamicPricingBatchStatus, type DynamicPricingContext, type DynamicPricingExecutionMode, type DynamicPricingInventoryGuardInput, type DynamicPricingInventoryGuardMode, type DynamicPricingInventoryMetric, type DynamicPricingMomentInput, type DynamicPricingTierContext, type DynamicPricingTierUpdateInput, type Echo, type EchoMediaAsset, type EchoType, type EditPartnerTicketTiersForMomentInput, type EventImportPlatform, GenqlError, type GetBrandEchoesForUserResponse, type GetMomentLineupsResponse, type GetMomentTagAssignmentsResponse, type GetPartnerAudienceInput, type GetPartnerAudienceResponse, type GetPartnerCustomersResponse, type GetPartnerDropsResponse, type GetPartnerMediaAssetsInput, type GetPartnerMediaAssetsResponse, type GetPartnerMembersInput, type GetPartnerMembersResponse, type GetPartnerMembershipTiersInput, type GetPartnerMembershipTiersResponse, type GetPartnerTicketTiersForMomentInput, type GetPartnerTicketTiersForMomentResponse, type GetPlansInput, type GetPlansResponse, type GetPublicMomentsResponse, type GetTicketsResponse, type ITMPartnerAgentAccessConfig, type ITMPartnerAgentAccessCredentials, type ITMPartnerAgentSessionConfig, type ITMPartnerAgentSessionCredentials, type ITMPartnerClient, type ITMPartnerClientConfig, type ITMPartnerHeaders, type ITMPartnerHeadersObject, type ITMPartnerTokenConfig, type ManualContributionReceiptType, type MediaAsset, type MediaAssetUploadUrl, type MembershipBillingInterval, type MembershipExpiryType, type MembershipPassType, type MembershipPurchaseType, type MembershipSource, type MembershipStatus, type MerchPurchase, type Moment, type MomentCategory, type MomentCollection, type MomentCollectionLink, type MomentDiscountCodeUsage, type MomentDynamicPricingPolicy, type MomentDynamicPricingTierRule, type MomentLineup, type MomentLineupDisplayLabel, type MomentLineupResponseResult, type MomentLineupStatus, type MomentLineupType, type MomentOccurrenceScheduleFrequency, type MomentOnlineProvider, type MomentPublicVenuePreview, type MomentSortOrder, type MomentStatus, type MomentSubcategory, type MomentTagAssignment, type MomentType, type MusicReleaseType, type Mutation, type PaginationInput, type PartnerAudienceCustomer, type PartnerAudienceFiltersInput, type PartnerAudienceSortField, type PartnerAudienceSortInput, type PartnerBrand, type PartnerCustomer, type PartnerMember, type PartnerMemberCustomer, type PartnerMemberTier, type PartnerMembershipTier, type PartnerMomentAttendanceCount, type PartnerMomentAttendanceLookupInput, type PartnerMomentAttendanceStats, type PartnerMomentGuestlistAttendance, type PartnerMomentGuestlistLabelAttendance, type PartnerMomentStats, type PartnerMomentTicketAttendance, type PartnerMomentTicketTierAttendance, type PartnerMomentsFiltersInput, type PartnerTicketStatus, type PartnerTicketTier, type PartnerTicketTierUpdateInput, type PartnerTicketUserLookupInput, type PartnerTicketsForUserInput, type Payment, type PaymentStatus, type Plan, type PlanBudget, type PlanBudgetAllocationType, type PlanBudgetArtistDealBasis, type PlanBudgetArtistDealInput, type PlanBudgetArtistDealType, type PlanBudgetCapacityAllocationInput, type PlanBudgetCategoryInput, type PlanBudgetCategoryKind, type PlanBudgetChargeCalculation, type PlanBudgetChargeKind, type PlanBudgetChargeTreatment, type PlanBudgetExternalTicketingChannelInput, type PlanBudgetFeeAssessment, type PlanBudgetFeeComponentKind, type PlanBudgetFeePercentageBasis, type PlanBudgetGuaranteeBasis, type PlanBudgetLineBasis, type PlanBudgetLineInput, type PlanBudgetPaymentStatus, type PlanBudgetPricingMode, type PlanBudgetSummary, type PlanBudgetTicketChargeRuleInput, type PlanBudgetTicketTierInput, type PlanBudgetTicketingChannelType, type PlanBudgetTicketingFeeComponentInput, type PlanKind, type PlanLinkMetadataStatus, type PlanMoodboard, type PlanMoodboardItem, type PlanMoodboardItemType, type PlanSchedule, type PlanScheduleInput, type PlanScheduleTime, type PlanScheduleTimeInput, type PlanScheduleType, type PlanStatus, type PlanSummary, type PlanningProposal, type PlanningSource, type PlanningSourceDispositionInput, type PlanningSourceSection, type PostSubscriptionEmailInput, type PostSubscriptionMessageInput, type PostSubscriptionPushInput, type PostSubscriptionSmsInput, type PreparePlanningProposalInput, type PublicMomentLineupInfo, type Query, type ReplacePlanBudgetDraftInput, type RespondToMomentLineupInput, type SendPartnerAgentEmailInput, type SortDirection, type SubscriptionComplianceInput, type Tag, type TagSource, type ThirdPartyEchoType, type Ticket, type TicketAdmissionWindowSource, type TicketAttendanceMode, type TicketExtension, type TicketExtensionStatus, type TicketTier, type Transforms, type UpdatePlanInput, type User, type UserSource, type Venue, type VenueInput, type VerifyPartnerBrandSubscriptionInput, createITMPartnerClient, everything, generateMutationOp, generateQueryOp };
3734
+ export { type AgentThreadSeedContext, type ApplyDynamicPricingInput, type BrandAgentEmailMessageInput, type BrandAgentEmailSendBatch, type BrandAgentEmailSendBatchStatus, type BrandAgentEmailSendMessage, type BrandAgentEmailSendMessageStatus, type BrandApp, type BrandAppStatus, type BrandDrop, type BrandDropCtaType, type BrandDropMusicLink, type BrandSubscription, type CheckoutMarketingConsentInput, type CheckoutMarketingConsentPrompt, type ConsentChannel, type ConsentPromptCategory, type ConsentStatus, type ContributionAdmin, type ContributionPageAdmin, type ContributionPageStatusFilter, type ContributionPagesResponse, type ContributionSource, type ContributionStatus, type ContributionsResponse, type CreateBrandAppInput, type CreateManualContributionInput, type CreateMomentOnlineConfigInput, type CreatePartnerBrandDropInput, type CreatePartnerBrandSubscriptionInput, type CreatePartnerBrandSubscriptionResponse, type CreatePartnerMomentInput, type CreatePartnerTicketTierInput, type CreatePlanBudgetInput, type CreatePlanInput, type Currency, DEFAULT_API_URL, type Dimensions, type DimensionsInput, type DynamicPricingActorType, type DynamicPricingAdmissionWindow, type DynamicPricingBatch, type DynamicPricingBatchEvent, type DynamicPricingBatchEventType, type DynamicPricingBatchItem, type DynamicPricingBatchItemStatus, type DynamicPricingBatchStatus, type DynamicPricingContext, type DynamicPricingExecutionMode, type DynamicPricingInventoryGuardInput, type DynamicPricingInventoryGuardMode, type DynamicPricingInventoryMetric, type DynamicPricingMomentInput, type DynamicPricingTierContext, type DynamicPricingTierUpdateInput, type Echo, type EchoMediaAsset, type EchoType, type EditPartnerTicketTiersForMomentInput, type EventImportPlatform, GenqlError, type GetBrandEchoesForUserResponse, type GetMomentLineupsResponse, type GetMomentTagAssignmentsResponse, type GetPartnerAudienceInput, type GetPartnerAudienceResponse, type GetPartnerCustomersResponse, type GetPartnerDropsResponse, type GetPartnerMediaAssetsInput, type GetPartnerMediaAssetsResponse, type GetPartnerMembersInput, type GetPartnerMembersResponse, type GetPartnerMembershipTiersInput, type GetPartnerMembershipTiersResponse, type GetPartnerTicketTiersForMomentInput, type GetPartnerTicketTiersForMomentResponse, type GetPlansInput, type GetPlansResponse, type GetPublicMomentsResponse, type GetTicketsResponse, type ITMPartnerAgentAccessConfig, type ITMPartnerAgentAccessCredentials, type ITMPartnerAgentSessionConfig, type ITMPartnerAgentSessionCredentials, type ITMPartnerClient, type ITMPartnerClientConfig, type ITMPartnerHeaders, type ITMPartnerHeadersObject, type ITMPartnerTokenConfig, type LoginCodeChannel, type ManualContributionReceiptType, type MediaAsset, type MediaAssetUploadUrl, type MembershipBillingInterval, type MembershipExpiryType, type MembershipPassType, type MembershipPurchaseType, type MembershipSource, type MembershipStatus, type MerchPurchase, type Moment, type MomentCategory, type MomentCollection, type MomentCollectionLink, type MomentDiscountCodeUsage, type MomentDynamicPricingPolicy, type MomentDynamicPricingTierRule, type MomentLineup, type MomentLineupDisplayLabel, type MomentLineupResponseResult, type MomentLineupStatus, type MomentLineupType, type MomentOccurrenceScheduleFrequency, type MomentOnlineProvider, type MomentPublicVenuePreview, type MomentSortOrder, type MomentStatus, type MomentSubcategory, type MomentTagAssignment, type MomentType, type MusicReleaseType, type Mutation, type PaginationInput, type PartnerAppLogin, type PartnerAppLoginChallenge, type PartnerAppLoginProfile, type PartnerAudienceCustomer, type PartnerAudienceFiltersInput, type PartnerAudienceSortField, type PartnerAudienceSortInput, type PartnerBrand, type PartnerCustomer, type PartnerMember, type PartnerMemberCustomer, type PartnerMemberTier, type PartnerMembershipTier, type PartnerMomentAttendanceCount, type PartnerMomentAttendanceLookupInput, type PartnerMomentAttendanceStats, type PartnerMomentGuestlistAttendance, type PartnerMomentGuestlistLabelAttendance, type PartnerMomentStats, type PartnerMomentTicketAttendance, type PartnerMomentTicketTierAttendance, type PartnerMomentsFiltersInput, type PartnerTicketStatus, type PartnerTicketTier, type PartnerTicketTierUpdateInput, type PartnerTicketUserLookupInput, type PartnerTicketsForUserInput, type Payment, type PaymentStatus, type Plan, type PlanBudget, type PlanBudgetAllocationType, type PlanBudgetArtistDealBasis, type PlanBudgetArtistDealInput, type PlanBudgetArtistDealType, type PlanBudgetCapacityAllocationInput, type PlanBudgetCategoryInput, type PlanBudgetCategoryKind, type PlanBudgetChargeCalculation, type PlanBudgetChargeKind, type PlanBudgetChargeTreatment, type PlanBudgetExternalTicketingChannelInput, type PlanBudgetFeeAssessment, type PlanBudgetFeeComponentKind, type PlanBudgetFeePercentageBasis, type PlanBudgetGuaranteeBasis, type PlanBudgetLineBasis, type PlanBudgetLineInput, type PlanBudgetPaymentStatus, type PlanBudgetPricingMode, type PlanBudgetSummary, type PlanBudgetTicketChargeRuleInput, type PlanBudgetTicketTierInput, type PlanBudgetTicketingChannelType, type PlanBudgetTicketingFeeComponentInput, type PlanKind, type PlanLinkMetadataStatus, type PlanMoodboard, type PlanMoodboardItem, type PlanMoodboardItemType, type PlanSchedule, type PlanScheduleInput, type PlanScheduleTime, type PlanScheduleTimeInput, type PlanScheduleType, type PlanStatus, type PlanSummary, type PlanningProposal, type PlanningSource, type PlanningSourceDispositionInput, type PlanningSourceSection, type PostSubscriptionEmailInput, type PostSubscriptionMessageInput, type PostSubscriptionPushInput, type PostSubscriptionSmsInput, type PreparePlanningProposalInput, type PublicMomentLineupInfo, type Query, type ReplacePlanBudgetDraftInput, type RespondToMomentLineupInput, type SendPartnerAgentEmailInput, type SortDirection, type StartPartnerAppLoginInput, type SubscriptionComplianceInput, type Tag, type TagSource, type ThirdPartyEchoType, type Ticket, type TicketAdmissionWindowSource, type TicketAttendanceMode, type TicketExtension, type TicketExtensionStatus, type TicketTier, type Transforms, type UpdatePlanInput, type User, type UserSource, type Venue, type VenueInput, type VerifyPartnerAppLoginInput, type VerifyPartnerBrandSubscriptionInput, createITMPartnerClient, everything, generateMutationOp, generateQueryOp };