Skip to content

SDK regeneration (TypeScript generator 3.107.1) - #533

Open
anubhav-intercom wants to merge 4 commits into
masterfrom
regen/ts-generator-3.107.1
Open

anubhav-intercom wants to merge 4 commits into
masterfrom
regen/ts-generator-3.107.1

Conversation

@anubhav-intercom

@anubhav-intercom anubhav-intercom commented Oct 23, 2026 •

Copy link
Copy Markdown

Why?

The SDK was last generated with TypeScript generator 3.35.8. Regenerating on 3.107.1 picks up the generator's fixes to the shared request code, including correct encoding of values placed in request paths.

How?

Regenerated from the same API spec as 7.0.3 and brought the core tests in line with the new generated code. Released as 7.1.0 rather than a patch because query parameters set to null are now left out of the request instead of being sent empty.

Notes for reviewers
  • Hand-maintained after regeneration (listed in .fernignore): README.md, src/BaseClient.ts, src/api/index.ts, src/api/types/compat.ts, src/auth/BearerAuthProvider.ts, src/core/fetcher/makePassthroughRequest.ts, src/core/url/QueryStringBuilder.ts. Compare them with the generated output on the next regeneration. BaseClient.ts also holds the SDK version string.
  • Custom fetchers get queryParameters again, rebuilt from the query string, so values arrive as strings (per_page: "5").
  • In the unstable namespace, two request types are renamed and some request arguments become required.

Generated with Claude Code

anubhav-intercom and others added 2 commits October 23, 2026 02:30
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@levindixon levindixon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran this against master (identical to the published 7.0.3) for breaking changes and the version bump. Everything below is against 36dd082. Version strings agree on 7.1.0 in package.json, src/version.ts and both SDK headers, and 7.1.0 is unpublished on npm. CI workflows are disabled (#523), so build, unit (332 tests) and wire (966 tests, against the unchanged 7.0.3 fixtures) ran locally, all green, plus a set of consumer compile and runtime probes against both builds (script at the bottom).

Requesting changes. Two things block at any version, then there's a SemVer call on five compatibility changes.

Nearly all of it is generator output rather than anything hand-written in this PR. Flagging it here so we can decide what to patch locally behind .fernignore and what to take upstream to Fern.

Blocking: README ships imports that don't resolve

README.md is in the npm files list, so this goes out in the tarball.

  • Lines 38 and 96 import from "./src/Client", lines 51/64/77/131/226/310 import from "Intercom". Both should be "intercom-client", which doesn't appear anywhere in the new README.
  • Line 121 advertises import { AdminsClient } from 'Intercom/admins'. Even corrected to intercom-client/admins it fails with ERR_PACKAGE_PATH_NOT_EXPORTED, the exports map only has . and ./package.json. Either add the subpath exports or drop that example.
  • The Installation section (and the npm badge) from 7.0.3 are gone, and line 165 refers to the retryStatusCodes generator setting, which means nothing to a package consumer.
  • README isn't in .fernignore, so a hand fix gets overwritten on the next regeneration. Setting the npm package name in the Fern generator config is the durable fix, or .fernignore it and hand-edit.

Blocking: client.fetch() fails in its default configuration

All three are in generated core and reproduce with a mocked fetch. The README documents the method, so they'd ship as documented behaviour.

  1. No default environment. new IntercomClient({ token }).fetch('/contacts') throws Failed to parse URL in Node, and .fetch('https://api.intercom.io/contacts') on the same client goes out without an Authorization header, because the origin check has no base URL to compare against. Setting environment: IntercomEnvironment.UsProduction explicitly fixes both. Generated endpoints already fall back to that environment, so the passthrough should resolve it before the URL and auth checks (src/Client.ts:225 passes this._options.baseUrl ?? this._options.environment with no default).
  2. client.fetch('/contacts?per_page=5&starting_after=next') produces .../contacts%3Fper_page=5&starting_after=next. join() writes the input, query included, to URL.pathname (src/core/url/join.ts:24).
  3. A POST Request plus an init object ({ headers: {...} }) is sent as a GET with no body. Request properties are only copied when init is absent (makePassthroughRequest.ts:73). Native new Request(req, init) keeps the method and body and only replaces the fields init sets, so losing the content-type header there is expected, losing the method and body isn't.

Compatibility: compiles and runs on 7.0.3, doesn't on this branch

  1. P1, silent. Custom fetchers lose query params. Generated clients call (this._options.fetcher ?? core.fetcher)({...}) with queryString and never queryParameters (310 call sites, was 0). Fetcher.Args.queryParameters is still there as deprecated, so a fetcher: that builds its URL from it keeps typechecking and drops every filter and pagination param without an error. events.list({ email, type: 'user', per_page: 5 }) gives a custom fetcher queryParameters: undefined, queryString: "email=...&type=user&per_page=5". I'd treat this as breaking unless custom fetchers keep receiving queryParameters. Since every call site reads this._options.fetcher, wrapping the user's fetcher once where options are normalised (parse queryString back into queryParameters) covers it.
  2. P2. Three Intercom.* aliases are gone: CreateContactRequestTwo, CreateDataEventRequestTwo, UpdateVisitorRequestOne (TS2724). They were = unknown placeholders only used by the unstable clients, so impact is close to nil, but they're top-level stable exports, so the "stable surface unchanged" line in the description needs adjusting too. Three deprecated re-exports in a .fernignored file keeps this a minor.
  3. P2. token: () => process.env.INTERCOM_API_KEY no longer compiles under strict null checks (TS2322, sync and async). The type went from Supplier<BearerToken | undefined> to Supplier<BearerToken> | undefined (src/auth/BearerAuthProvider.ts:43). Runtime still falls back to the env var when the supplier returns undefined, so widening the type parameter back fixes it.
  4. P2. requestOptions.queryParams: { per_page: undefined } used to remove the endpoint param, now the endpoint value is kept (?type=user&per_page=5 vs 7.0.3's ?type=user&custom=). mergeAdditional in QueryStringBuilder.ts:65-74 skips nullish overrides instead of deleting the key, and its serialized.length > 0 guard also skips overrides that serialise to nothing (empty arrays). Deleting the key on undefined is a two-line fix. The null omission is in the description and is fine as a release note.
  5. P2, type-level. Response types moved from email?: string to email?: string | undefined, which breaks consumers on exactOptionalPropertyTypes (TS2375 / TS18048 against Intercom.Contact). I'd accept this one as a documented type-level change rather than a break: it only affects consumers on an opt-in compiler flag, and restoring it means overriding the generator's output across every response type. Flagging it so it's a deliberate call, strict SemVer would count it.

Two more type-level changes with no consumer repro, same treatment as 5: BaseClientOptions and every *Client.Options moved from interface to type (breaks declaration merging only), and Fetcher.Args.headers widened to Record<string, unknown> (breaks a custom fetcher that forwards args.headers to a narrower type).

Version

7.1.0 is the right size for what's additive (client.fetch(), auth option, additionalBodyParameters, stream, error.requestId / error.cause). It stays a minor if compat items 2, 3 and 4 are restored and item 1 gets the adapter. If the adapter isn't workable in generated core, I'd go 8.0.0 with migration notes rather than ship a silent request change under a minor. Item 5 and the two type-level changes go in the release notes either way. The release tag has to equal 7.1.0 (leading v optional) or publish.yml aborts.

Release-note items and nits (not blocking)

Consumers will notice these, none are breaking:

  1. IntercomTimeoutError now extends IntercomError (was Error), so instanceof IntercomError branches catch timeouts too. error.name is the class name instead of "Error", and the auth-missing message text changed.
  2. The default 20s timeout now covers reading the body for non-streaming responses (keepTimeoutUntilBodyRead, Fetcher.ts:188). 7.0.3 cleared it when headers arrived. Big responses on slow links can newly time out.
  3. Path params . and .. now throw. 2xx responses labelled JSON that fail to parse now throw instead of returning { ok: false }, which is a fix.
  4. Unstable: required request on createContact / createDataEvent / updateVisitor and messages.createMessage, required user / visitor on ConvertVisitorRequest, ReplyConversationRequest is now ReplyConversationBody and UpdateDataAttributeRequest is now UpdateDataAttributeBody. Matches the description and isn't covered by semver, just a line for unstable users.

Nit: wire tests weren't regenerated. They still pass against the 7.0.3 fixtures, which is decent evidence the wire format for the covered endpoints hasn't moved.

Reproduction script (mocked fetch, placeholder token, expects base/ and head/ builds with deps alongside)
const fs = require('node:fs');
const path = require('node:path');
const ts = require('./head/node_modules/typescript');

const cases = {
  stable_aliases: `
import type { Intercom } from 'SDK';
type ContactHelper = Intercom.CreateContactRequestTwo;
type EventHelper = Intercom.CreateDataEventRequestTwo;
type VisitorHelper = Intercom.UpdateVisitorRequestOne;
`,
  optional_token_supplier: `
import { IntercomClient } from 'SDK';
declare const readToken: () => string | undefined;
new IntercomClient({ token: readToken });
new IntercomClient({ token: async () => readToken() });
`,
  exact_optional_response: `
import type { Intercom } from 'SDK';
declare const contact: Intercom.Contact;
const email: { email?: string } = contact;
if ('email' in contact) contact.email.toUpperCase();
`,
  unstable_renames_and_required_requests: `
import { IntercomClient, type Intercom } from 'SDK';
declare const client: IntercomClient;
type Reply = Intercom.unstable.ReplyConversationRequest;
type Update = Intercom.unstable.UpdateDataAttributeRequest;
client.unstable.contacts.createContact();
`,
};

function compileCase(version, name, source) {
  const filename = path.join(__dirname, `${version}-${name}.ts`);
  fs.writeFileSync(filename, source.replaceAll('SDK', `./${version}`));
  const program = ts.createProgram([filename], {
    strict: true, noEmit: true, skipLibCheck: true,
    exactOptionalPropertyTypes: name === 'exact_optional_response',
    target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS,
    moduleResolution: ts.ModuleResolutionKind.NodeJs,
    typeRoots: [path.join(__dirname, 'head/node_modules/@types')],
  });
  return ts.getPreEmitDiagnostics(program).map(d => {
    const pos = d.file && d.start != null ? d.file.getLineAndCharacterOfPosition(d.start) : undefined;
    return { file: d.file && path.relative(__dirname, d.file.fileName), line: pos && pos.line + 1,
      code: d.code, message: ts.flattenDiagnosticMessageText(d.messageText, '\n') };
  });
}

async function runtimeCases(version) {
  const { IntercomClient } = require(`./${version}/dist/cjs/index.js`);
  const received = [];
  const client = new IntercomClient({ token: 'review-placeholder', fetcher: async args => {
    received.push({ queryParameters: args.queryParameters ?? null, queryString: args.queryString ?? null });
    return { ok: true, body: { type: 'event.list', events: [] }, rawResponse: { headers: new Headers(), status: 200 } };
  }});
  await client.events.list({ email: 'review@example.com', type: 'user', per_page: 5 });

  const urls = [];
  const fetchClient = new IntercomClient({ token: 'review-placeholder', fetch: async (url, init) => {
    urls.push(String(url));
    return new Response(JSON.stringify({ type: 'event.list', events: [] }), { headers: {'Content-Type': 'application/json'} });
  }});
  await fetchClient.events.list({ type: 'user', per_page: 5 }, {queryParams: { per_page: undefined, custom: null }});
  return { custom_fetcher: received, undefined_null_query_override: urls };
}

async function passthroughCases() {
  const { IntercomClient, IntercomEnvironment } = require('./head/dist/cjs/index.js');
  const observations = [];
  for (const explicitEnvironment of [false, true]) {
    const captured = [];
    const client = new IntercomClient({ token: 'review-placeholder',
      ...(explicitEnvironment ? {environment: IntercomEnvironment.UsProduction} : {}),
      fetch: async (url, init) => {
        captured.push({url: String(url), hasAuth: new Headers(init.headers).has('authorization')});
        new Request(url, init);
        return new Response('{}');
      },
    });
    for (const input of ['/contacts', 'https://api.intercom.io/contacts']) {
      try { await client.fetch(input); observations.push({explicitEnvironment, input, result: 'ok'}); }
      catch(error) { observations.push({explicitEnvironment, input, result: error.message}); }
    }
    observations.push({explicitEnvironment, captured});
  }
  return observations;
}

async function passthroughInputCases() {
  const { IntercomClient, IntercomEnvironment } = require('./head/dist/cjs/index.js');
  const captured = [];
  const client = new IntercomClient({ token: 'review-placeholder', environment: IntercomEnvironment.UsProduction,
    fetch: async (url, init) => {
      captured.push({url: String(url), method: init.method, hasBody: init.body != null,
        contentType: new Headers(init.headers).get('content-type')});
      return new Response('{}');
    },
  });
  await client.fetch('/contacts?per_page=5&starting_after=next');
  await client.fetch(new Request('https://api.intercom.io/contacts', {
    method: 'POST', body: JSON.stringify({email: 'review@example.com'}),
    headers: {'Content-Type': 'application/json'},
  }), {headers: {'X-Review': 'true'}});
  return captured;
}

(async () => {
  const results = {typechecks: {}, runtime: {}, passthrough: []};
  for (const [name, source] of Object.entries(cases)) {
    results.typechecks[name] = {};
    for (const version of ['base', 'head']) results.typechecks[name][version] = compileCase(version, name, source);
  }
  for (const version of ['base', 'head']) results.runtime[version] = await runtimeCases(version);
  results.passthrough = await passthroughCases();
  results.passthroughInputs = await passthroughInputCases();
  fs.writeFileSync(path.join(__dirname, 'compatibility-results.json'), JSON.stringify(results, null, 2) + '\n');
  console.log(JSON.stringify(results, null, 2));
})().catch(error => { console.error(error); process.exitCode = 1; });

~ Automated via Claude

Restore package-user README, default environment and query/Request handling in client.fetch(), query parameters for custom fetchers, deprecated type aliases, optional token supplier typing and queryParams removal semantics. Fernignore the patched files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@levindixon levindixon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-ran everything against d167574: build, unit (347, the 332 from before plus the 15 new ones), wire (966, unchanged fixtures) and the consumer probes from the first review. Everything from that review is addressed. README imports resolve, Installation and the badge are back, client.fetch() resolves the US environment and keeps query strings out of the path, the Request merge now matches new Request(req, init), custom fetchers get queryParameters again, the three aliases and the wider token type compile under strict, and queryParams: { x: undefined } removes the param. 7.1.0 holds.

The adapter plus .fernignore approach is the one I was hoping for, and the new tests pin each item down individually, which makes the next regeneration much easier to check.

Requesting changes for one bug in the new fetcher adapter. It's a one-line fix, then this is an approve from me.

Bug: repeated query keys reach custom fetchers with the first value split into characters

In withQueryParameters (src/BaseClient.ts), the second occurrence of a key does [...(existing as string[]), value], but existing is still a plain string at that point, so it spreads into characters. Through the built SDK, events.list({ type: "user" }, { queryParams: { tags: ["ab", "cd"] } }) reaches the fetcher as tags: ["a", "b", "cd"]. The new test only uses ["a", "b"], which is why it passes.

Fix is Array.isArray(existing) ? [...existing, value] : [existing, value], plus values longer than one character in the test.

Concern: .fernignore now covers src/api/types/index.ts

That's the 160-line barrel that gets a new export * line for every type the API adds. Once it's ignored, the next regeneration writes the new type files but the barrel stays frozen, so new types stop being exported from Intercom.* and nothing fails to tell us.

I'd move the three aliases into a new fernignored src/api/types/compat.ts and re-export it from src/api/index.ts (four lines, changes far less often), ignoring that file instead of the barrel. If you'd rather keep the barrel ignored, a comment at the top saying it has to be diffed by hand after each regeneration would be the minimum.

Nits, none blocking:

  • queryParameters values are now strings (per_page: "5" where 7.0.3 passed 5). Fine for fetchers that serialise straight to a URL. A line in the release notes or on the wrapper would save someone a debugging session.
  • makePassthroughRequest reads input.arrayBuffer() even when init.body replaces it, so the caller's Request ends up bodyUsed. Native new Request(req, init) leaves it alone. Guarding on init?.body === undefined matches it.
  • With the US default, client.fetch("https://api.eu.intercom.io/...") goes out without Authorization unless environment is set to EU. That's the right behaviour, but the Custom Fetch section of the README says "benefiting from ... authentication" without the condition. One sentence there would head off the question.
  • The PR description could mention the compat patch and the six fernignored files, so whoever runs the next regeneration knows those are hand-maintained.

Unrelated to the change, but since CI is off: on Node 25 the wire suite fails at import with localStorage.getItem is not a function (msw 2.11 meets the new localStorage global). NODE_OPTIONS=--no-experimental-webstorage gets all 966 passing.

~ Automated via Claude

…ut of the types barrel

The second value of a repeated key was spread into characters. Leave a Request body unread when init replaces it, keep the generated types barrel regenerable by moving the 7.0.x aliases to a fernignored compat file, and note in the README that auth only goes to the configured environment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@anubhav-intercom

Copy link
Copy Markdown
Author

Fixed the repeated-key bug and moved the aliases to a fernignored compat.ts, so the types barrel regenerates again. The nits are in too.

~ Automated via Claude

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants