Share client-sdk runAs/options type and guard stub drift (#24716)
## What & why
Follow-up to #24671. The pre-generation `CoreApiClient` stub is a
hand-kept copy of the generated client's constructor signature, and
nothing enforced they agree. Apps typecheck against the stub but run
against the generated client that `twenty dev` swaps in, so an option
added to one and not the other (this happened with `runAs` in #24493)
compiles green yet fails app typecheck against the stub while working at
runtime.
The `runAs` type and options shape were hand-declared in four places,
free to drift:
- `generate/twenty-client-template.ts` (the generated client template)
- `core/generated/index.ts` (the stub)
- `metadata/generated/index.ts` (committed genql snapshot)
- `rest/index.ts`
## Changes
- **New `shared/twenty-client-options.ts`**: one source of truth
exporting `TwentyClientRunAs` and `TwentyClientOptions`. It mirrors the
genql `ClientOptions` shape without depending on the generated genql
types (which do not exist until `twenty dev` runs).
- **Stub** (`core/generated/index.ts`): constructor now accepts
`_options?: TwentyClientOptions` instead of no args, so `new
CoreApiClient({ runAs: 'application' })` typechecks. It still throws the
identical `CoreApiClient was not generated.` error, so runtime behavior
is unchanged.
- **Rest client** (`rest/index.ts`): sources `TwentyClientRunAs` from
the shared module and re-exports it (preserving the public
`twenty-client-sdk/rest` export), dropping its local copy.
- **Template** (`generate/twenty-client-template.ts`): references
`TwentyClientRunAs` via a type-only import and exports
`TwentyGeneratedClientOptions` for the guard. It keeps `ClientOptions &
{ runAs }` (genql's wide `ClientOptions`) so the generated client still
forwards the full `RequestInit` passthrough, no behavior change.
- **Drift guard**
(`generate/__tests__/stub-generated-client-drift.test.ts`): a pure
type-level assertion (option-name-set equality plus bidirectional
assignability between the stub's constructor options and the template's
generated options) that fails `tsgo` in this package's own CI the moment
the two diverge, plus a runtime test constructing the stub with the full
option set and asserting the generation error still throws.
## The shared import and the standalone template
The template's post-injection output ships standalone into the installed
SDK (it replaces `dist/core.mjs`/`dist/core.cjs`), so a shared import
could dangle after generation. The reference is therefore **type-only**:
it survives injection into the generated source, but esbuild elides
`import type` without resolving the path, so the shipped bundle stays
self-contained. Verified by running the real generation pipeline: the
import appears in the generated source and is absent from the bundle,
while `CoreApiClient` and the `runAs` env handling assemble correctly.
`metadata/generated/index.ts` is a committed genql snapshot
(`@ts-nocheck`) that only regenerates against a live server, so it is
left untouched; its duplicated `runAs` is inert and it will pick up the
shared import on its next regeneration.
## Verification
- `nx run twenty-shared:build --skip-nx-cache` — success
- `tsgo --noEmit -p tsconfig.lib.json` — clean
- `tsgo --noEmit -p tsconfig.json` (what `nx typecheck` runs; includes
tests) — clean
- Drift check: temporarily narrowing the stub to drop `runAs` fails
typecheck at both the type-level assertion and the construction;
restoring is clean
- `nx test twenty-client-sdk` — passing
- `nx lint twenty-client-sdk` — 0 warnings, 0 errors
- `nx build twenty-client-sdk --skip-nx-cache` — success; generation
pipeline exercised end-to-end
- Slack app (consumes the stub) shows no
`CoreApiClient`/twenty-client-sdk typecheck errors from this change; its
`runAs` cast could now be dropped, left to its own PR
---
_Generated by [Claude
Code](https://claude.ai/code/session_012kdQ5ksf59EVebL9DWREfN)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/24716?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. --> A
Abdul Rahman committed
18df4ff55b8a5d71d41b2f22fc1342ab92cfe5bc
Parent: 8bbcda4
Committed by GitHub <noreply@github.com>
on 8/25/2026, 11:12:14 AM