Этот раздел генерируется из исходников SDK (на английском языке) и является эталоном. Перевод приводится по мере обновлений.
All notable changes to @tv/extension-sdk.
The format follows Keep a Changelog; the SDK uses Semantic Versioning from 1.0 onward.
[Unreleased]
Fixed
publishConfig.registrypointed at public npmjs, where@tv/*has never been published and the scope isn't even ours (curl https://registry.npmjs.org/@tv%2fextension-sdk→ 404; the@tvorg page → 403). Every real publish has always gone to the private Verdaccio (https://npm.k8s.tangovision.dev/) — manual publishes because the@tv:registryscope mapping in~/.npmrcoverrides bothpublishConfigand an explicit--registryflag for scoped packages (verified during the 1.11.0 release:npm publish --registry=https://registry.npmjs.org/still PUT to Verdaccio), and CI publishes becausepublish-extension-sdk.ymlintv-platformgets its registry fromactions/setup-node'sregistry-url+scope: '@tv'inputs, which write the same kind of scope-specific mapping — not frompublishConfig. So the stale value was never actually reachable, only misleading: a reader (or an agent) who trusted it would conclude the package is public, and an environment without the scope mapping would have sent a real publish attempt at a registry where we don't control the scope.publishConfig.registrynow readshttps://npm.k8s.tangovision.dev/, matching every other reference to the registry in this repo (README, AGENTS.md). Checked and unaffected: the generated.npmrctemplate (scaffoldNpmrc()insrc/manifest/scaffold.ts) intentionally keeps a bareregistry=https://registry.npmjs.org/alongside the@tv:registry=override — that's the correct shape (unscoped deps from public npm,@tv/*from Verdaccio), not the same bug. No otherpublishConfigblock exists in this repository.
Sales Impact
- Internal correctness fix to a package manifest field; no customer-facing behavior changed. Omitted per AGENTS.md §3.3.
[1.11.0] — 2026-08-29
Changed
sandbox createnow polls the provisioner instead of holding one HTTP request open for the whole provision. The provisioner answersPOST /api/v1/sandboxeswith202 Acceptedand a record inprovisioningstatus (tv-platform#658); the CLI pollsGET /api/v1/sandboxes/:idevery 5s (progress line every ~30s in human mode, silent in--format=json) until the status turnsreadyorfailed, giving up with exit 2 after 15 minutes — provisioning continues server-side andsandbox list/sandbox connect <id>pick it up later. Old synchronous provisioners still work: areadyresponse skips the polling loop, and when the create call is cut off mid-flight (the pre-202 failure mode — the ingress used to 504 a create that then succeeded server-side) the CLI reconciles by display name viaGET /api/v1/sandboxesand resumes watching the sandbox it finds. New exports for tooling that drives creates itself:waitForSandboxTerminal,findLatestSandboxByName,SandboxWaitTimeoutError.
Sales Impact
- Persona: external module developer on a slow or proxied network.
- Problem Solved:
sandbox createno longer reports a scary error for a create that actually succeeded — the command now reliably ends with the sandbox's real outcome, however long seeding takes. - Pitch Hook: kick off a sandbox and watch it come up — the CLI tracks provisioning to completion instead of gambling on one long HTTP request.
[1.10.0] — 2026-08-29
Added
login/logout/whoami— the interactive credential path (Phase 3.5 of the Module Developer Platform initiative).npx @tv/extension-sdk logincompletes the OAuth 2.0 Device Authorization Grant (RFC 8628) against thedevelopersKeycloak realm: it opens the browser to the verification page (printing the URL and user code as the fallback —--no-browserskips the launch), polls the token endpoint with the RFC's pacing rules, and stores the resulting tokens in the OS keyring via@napi-rs/keyring— macOS Keychain, Linux Secret Service, Windows Credential Manager. Where no keyring is usable (headless Linux without a D-Bus secret service, unsupported platforms) the tokens fall back to~/.config/tv-sdk/credentials.jsonwith0600permissions and a stderr warning;TV_SDK_TOKEN_STORE=file|keyringforces the backend —fileis the right setting for CI,keyringturns a silent fallback into a hard error. The flow requestsoffline_accessdeliberately: without it the refresh token dies with the realm's 30-minute SSO idle timeout, with it the session survives until 30 days unused (the realm's offline idle limit).logoutrevokes the refresh token server-side (RFC 7009) and clears local storage even when the realm is unreachable;whoamiproves the whole chain by round-tripping storage → refresh → userinfo. Tokens are never printed. The Keycloak side is a dedicated public clienttv-sdk-cliwith ONLY the device grant enabled (standard flow and direct grants off) and the samedeveloper-apiaudience mapper asdeveloper-portal, so provisioner-side JWT verification needed no changes. Limits worth knowing: login is the interactive path —tvk_…API keys minted in the developer portal remain the non-interactive credential for CI, and the account must exist in the invite-only developers realm first.sandboxlive subcommands now actually call the deployed provisioner.list,connect,extend,delete,reset, and livecreatewere "service is not yet deployed" stubs from Phase 1a.4; they now callhttps://sandbox-api.k8s.tangovision.dev(override with--api,TV_SANDBOX_API_URL, orTV_API_URL, in that order — the localhost default is gone now that the service is real). Credential resolution, in order:--tokenflag,TV_API_TOKENenv, then the storedloginsession with automatic token refresh — so a logged-in developer runsnpx @tv/extension-sdk sandbox create …with no env setup at all, while existing API-key workflows keep working unchanged. New--ttl-days=Nflag oncreate(1–30, service default 14). Distinct failure modes are distinguished: 401 tells you which credential path was rejected and what to do, quota refusals surface the provisioner's structured message, and an unreachable service still exits 2 where a refused request exits 1.init-modulescaffolds the ADR 103 federation-safe i18n setup, and the module it emits now builds. Three things every generated frontend is born with, ported from the canonical tv-module-example:src/shims/use-sync-external-store-shim.ts(an import-then-re-export ofuseSyncExternalStorefromreact— a plainexport { x } from 'react're-export is NOT rewritten by the federation plugin and reintroduces the second-React crash), the matchingresolve.aliasforuse-sync-external-store/shimin the generatedvite.config.ts, and a generated.github/workflows/ci.ymlwhose frontend checks job builds the real federation remote and mounts it in the shell-integration smoke (tangovision/infrastructure/.github/actions/shell-smoke@master, report-only). Without the alias, every scaffold carrying react-i18next ≥ 16 shipped the 2026-08-27 incident by construction; the smoke is the only CI signal that can see that failure class.For that CI to be green on the first push, the scaffold also gains the files a generated module was missing to build at all:
tsconfig.json(the generatedtsc -b && vite build/tsc --noEmitscripts had nothing to run against),index.html+src/main.tsx(the standalone dev entryvite buildneeds),.npmrc(without it the firstpnpm installresolves@tv/extension-sdkagainst npmjs and 404s), andsrc/locales/catalogues.test.ts— the key-parity test the generatedsrc/i18n.tsdocblock was already promising ("the parity check in CI will hold you to it") without shipping it. New exports:scaffoldShim(),scaffoldCiWorkflow(),scaffoldTsconfig(),scaffoldIndexHtml(),scaffoldMainEntry(),scaffoldNpmrc(),scaffoldCatalogueTest().init-moduleemits a.gitignore. A freshly scaffolded module had none, so its very first commit could takenode_modules/,dist/and coverage output with it — and because ignoring an already-tracked path does not untrack it (platform rule 3.1), that mistake is permanent until someone runsgit rm --cached, which is exactly the cleanup the 2026-08 fleet sweep spent on repos born this way. The generated rules cover install output, build output (dist/,*.tsbuildinfo), coverage, logs and local.envfiles, and deliberately do NOT ignorepnpm-lock.yaml: the generated CI installs with--frozen-lockfile, which fails on a repo without a committed lockfile. Exported asscaffoldGitignore(). (Extracted from the still-useful part of #82; that PR's i18n half predates ADR 103 and was superseded by the shim + alias work above.)
Changed
The shipped platform rules state that
pnpm.overridesreplaces, rather than merges. Re-syncedPLATFORM_RULESfromtangovision/templates/PLATFORM-RULES.md(templates#32). The override-floor bullet previously stopped at "pnpm-workspace.yaml, notpackage.json", which is correct advice and still misses the way the floors actually came off: a non-emptypnpm.overridesin an install root'spackage.jsonreplaces that root'spnpm-workspace.yamloverrides:block wholesale, not key by key, so a floor written in only one of the two files is not a floor at all. Both files may legitimately carry the block —pnpm install --ignore-workspacedoes not readpnpm-workspace.yaml— but then they must be kept byte-identical, andERR_PNPM_LOCKFILE_CONFIG_MISMATCHmust never be answered with--no-frozen-lockfile, which turns the loud failure green and the silent downgrade permanent. tv-platform#598 pasted a generated floor list into 13package.jsonfiles and made six workspace blocks inert this way; intv-module-notificationsadeepmerge-tsCVE floor silently weakened from^8.0.1to>=7.1.5(GHSA-ggr8-5vv4-36mx) while the comment above it still claimed the two files were in sync (tv-platform#615). Full rationale and theoverride-source.ymlgate that detects it:tangovision/infrastructure/docs/ci-gates.md#the-pnpm-override-source-invariant(infrastructure#414).Scaffolded modules pin
react-i18nextat^17.0.0(was^16.0.0) — the major ADR 103 verified as safe in both sanctioned i18n models and what tv-module-example runs. Safe only together with the shim alias above; neither half may be dropped without the other.buildingTypesdocuments where its vocabulary is decided. The field's slugs are validated at ingest, against tv-api'sbuilding_typesregistry — the platform's single source of truth for building types (ADR 097 layer 2, seeded in tv-api#162).POST /api/v1/registry/modules/ingestnow rejects a manifest naming a type that does not exist, listing the valid values; before, a typo ingested happily and gated the module to nothing, silently and permanently.The Zod schema deliberately does not enumerate the slugs. This package ships on its own release cadence, so an enum here would be a fourth copy of a list that already disagreed with itself in three places, and a module could not adopt a newly added building type until the SDK caught up. The schema keeps validating slug shape; the registry decides slug membership.
Security
- The copilot's
init_moduletool and the RU contracts doc invoke the CLI by its full package name. Both used barenpx tv-sdk— the doc as an instruction, the copilot as a string passed toexecSync. The package nametv-sdkis unclaimed on public npm (npm view tv-sdk→ 404), and outside a directory with@tv/extension-sdkinstalled locally, npx resolves a bare name against registry.npmjs.org — so a squatter publishingtv-sdkwould have had their code executed by every developer following the checklist, and by the copilot itself when run outside such a directory. Both sites now spellnpx @tv/extension-sdk <cmd>, which resolves through the authenticated@tvregistry wherever it runs; since the package declares a single bin, the two forms are otherwise identical. The developer-docs guides carried the same instruction in 34 places (fixed in tv-platform#647), andinfrastructure/docs/ci-gates.mdin one.
Sales Impact
- Persona: external module developers and ISV partners in the developer-platform preview.
- Problem Solved: getting a credential into the CLI used to mean signing in to the portal, minting an API key, and pasting it into environment variables before the first sandbox command could run — and that key then lived in shell history or a dotfile. Now it is one browser sign-in:
npx @tv/extension-sdk login, and every sandbox command authenticates itself from the OS keyring. - Pitch Hook: "Sign in once in your browser and the SDK remembers you — securely, in your OS keyring, for up to 30 days of inactivity. No tokens in dotfiles, and
logoutrevokes the session server-side, not just locally." (Scope of the claim: the interactive CLI path in the invite-only preview; CI and other non-interactive callers still use portal-minted API keys.)
[1.9.0] — 2026-08-25
Added
init-moduleemitsAGENTS.mdandCLAUDE.md. The AGENTS.md opens with the platform rules fromtangovision/templates/PLATFORM-RULES.md(English only, callbuild-push.yml,setup-pnpm/setup-tv-registry, org secrets, override floors, shared-runner constraints), so an agent working in a scaffolded module repo can see rules that otherwise live only in theceomonorepo. Exported asscaffoldAgentsMd(),scaffoldClaudeMd()and thePLATFORM_RULESconstant.
[1.8.0] — 2026-08-20
Added
building.fault.createdjoins the event catalog. tv-api's FDD engine has emitted this event since the fault/work-order bridge landed —src/fdd/fdd.service.tspublishes it immediately afterbuilding.alarm.triggeredwhenever a diagnostic rule opens a fault — but it was never declared inEVENT_CATALOG, so it was absent fromtv-events.snapshot.jsonand invisible to every piece of tooling built on that snapshot.The visible symptom was in
check-pact:@tv/module-cafmdeclares a fullpayloadContracton it (Auto-create a work order from an FDD fault) and was toldEVENT_NOT_PUBLISHED— rule 1, the diagnosis reserved for a consumer subscribing to an event nothing emits. That was wrong on the facts. The producer exists, is wired end to end (EventBusService.publishroutes it totv.building.<buildingId>.building.fault.created, which is exactly the subject CAFM'sTicketBridgeServicesubscribes to), and CAFM's handler, dedup path,faultIdcolumn and unit tests are all in place. Only the declaration was missing. tv-platform'smanifest-fleet-gatecarried the gap on its allowlist; that line comes off with this release.The payload is transcribed from the emit site rather than from the consumer's expectations:
faultId,buildingId,severity,titleanddetectedAtare required (all non-null in thefaultstable, anddetectedAtdefaults tonow()), whileelementIdandpointIdare optional (both nullable columns, emitted as?? undefined).severityis deliberately an openz.string().min(1)and not an enum likebuilding.alarm.triggered's. Thefaults.severitycolumn is a freeStringwritten by rule definitions, and tv-api's own summary code guards withif (f.severity in bySeverity)instead of assuming the set — so an enum here would advertise a guarantee the producer does not make, and would hand consumers a falsePRODUCER_EMITS_UNHANDLED_ENUMsignal. Common values are critical/high/medium/low; CAFM's severity mapping already falls back for anything else.
[1.7.0] — 2026-08-20
Added
init-moduleemits apnpm-workspace.yamlwith the fleet's build-script approval lists. A freshly scaffolded module was born unable to run the verypnpm installthe CLI prints as its next step: pnpm 11 hard-fails install withERR_PNPM_IGNORED_BUILDSon any dependency whose build script is not approved (pnpm 10, the platform CI pin, only warns), and the scaffold's own tree contains one such package — esbuild, via vite. This is the same failure that had to be fixed one repo at a time across all eleven existingtv-module-*repositories (e.g. tv-module-cafm#79, tv-module-leases#89/#90).The generated file carries the approvals in the fleet's dual-list convention —
allowBuilds(a map, read by pnpm 11) mirrored byonlyBuiltDependencies(a list, read by pnpm 10), alphabetical — both rendered from a single exported constant,APPROVED_BUILD_PACKAGES, so the generated lists cannot drift apart the way hand-mirrored ones have. The set is the platform's canonical one (what tv-module-cafm and tv-module-example carry), not the scaffold's minimal need: beyond esbuild it pre-approves what the backend every module grows next actually pulls in —@prisma/engines(prisma postinstall) andunrs-resolver(jest ≥ 30.4's native resolver) above all.@scarf/scarfis deliberately not listed: the scaffold has no@nestjs/swagger, and the file's comment instructs the author to add the fleet's explicit block ('@scarf/scarf': false+ignoredBuiltDependencies) rather than an approval if swagger ever brings it in.The scaffold-canary workflow gains an install of the generated module under a pinned pnpm 11.18.0. The existing pnpm 10 install cannot regress this guarantee — 10 only warns — so without it the failure would quietly return the first time the scaffold's dependency tree grows a new build-script package.
[1.6.1] — 2026-08-20
Fixed
tv-sdk check-pactresolves its default snapshot without assuming cwd. The default producer snapshot was a single cwd-relative path,packages/tv-extension-sdk/tv-events.snapshot.json, which only exists when the command runs from the tv-platform meta-repo root. Runningcheck-pactfrom inside the SDK repo — wheretv-events.snapshot.jsonsits in the repo root — failed withENOENT, and the standing workaround was to pass--snapshot=tv-events.snapshot.jsonby hand. Pre-existing; 1.6.0 left it alone to keep the--producerchange scoped.The default is now a search over candidates, most-authoritative first: the meta-repo layout relative to cwd (unchanged, still the common case in CI), then the snapshot bundled with the running SDK install, then an upward walk from cwd checking both the bare filename and the meta-repo layout at each ancestor. The bundled candidate is resolved from the module's own URL rather than from cwd — cwd is the thing that cannot be trusted here — which is what makes the SDK repo, an
npx @tv/extension-sdkinstall, and vitest running the TypeScript source all land on the right file.tv-events.snapshot.jsonis listed in package.jsonfiles, so it is present at that root in a published tarball too.An explicit
--snapshot=<path>still wins outright and is still what the error tells you to reach for, but the failure now lists every candidate that was tried instead of naming one path that was never going to exist.check-eventsandsnapshot-eventswere checked for the same defect and do not share it: both take a required positional path, andsnapshot-events' path is a write target, where an upward search would be wrong. They are unchanged.Resolution moved out of
cli.tsintosrc/events/snapshotPath.ts(findDefaultSnapshotPath(),defaultSnapshotCandidates()) so it is unit-testable —cli.tsrunsprocess.exit()on import. It is deliberately not re-exported from@tv/extension-sdk/events, which module frontends import and which must stay bundler-safe; this file reaches fornode:fs.
[1.6.0] — 2026-08-20
Added
Modules can act as event producers in
check-pact. The pact checker built its producer view solely fromEVENT_CATALOG— tv-api's first-party events. Module-published events are declared inmodule-manifest.jsonunderevents.publishesand never appear in that catalog by design, so a consumer declaring apayloadContracton one tripped rule 1 (EVENT_NOT_PUBLISHED) and failed CI with no correct way out.tv-sdk check-pactgains a repeatable--producer=<manifest>flag that federates a producing module'sevents.publishesinto the producer view. The first-party snapshot is merged first, so tv-api keeps ownership if a module ever collides with a catalog event; a second claim on the same event name is reported as a conflict rather than silently overwriting.The alternative — grafting module events into
EVENT_CATALOG— was rejected: it misattributes them to tv-api, couples a module's contract to the SDK release cycle, and enforces nothing at runtime, since the catalog has no runtime consumer and feeds snapshot/CI tooling only.New exports on
@tv/extension-sdk(src/events/manifestProducer.ts):buildModuleProducerSnapshot()converts a manifest'sevents.publishes[*].payloadContractinto the snapshot shapebuildEventSchemaSnapshot()emits, andmergeProducerSnapshots()unions producer snapshots. Both are pure and takegeneratedAtfrom the caller so regenerated output stays byte-identical.payloadContractis now symmetric. Onevents.subscribes[*]it remains the subset of a producer's payload the consumer reads; onevents.publishes[*]it describes what the module guarantees to emit. Same wire format and no schema change —manifest.schema.jsonandtv-events.snapshot.jsonare byte-identical to 1.5.0.A
publishesentry without apayloadContractstill registers the event, deliberately: the module does publish it, so a subscriber must never be toldEVENT_NOT_PUBLISHED. Such an entry carries an explicitx-tv-opaquemarker (OPAQUE_PAYLOAD_MARKER) and surfaces as a new info-levelPRODUCER_PAYLOAD_OPAQUEdiff, making the coverage gap visible without turning CI red. The marker is explicit rather than inferred from a missingpropertieskey, because that key is also absent on a corrupt or truncated snapshot — which must keep reporting breaking diffs as it always has.
[1.5.0] — 2026-08-19
Added
Shared
<LocationPicker>on@tv/extension-sdk/react. The storey → space → optional element cascade that CAFM and service-desk each hand-rolled over the same three graph endpoints is now one component (ADR 094 phase 5). It runs over anyBuildingGraphClientpassed as a prop, queries throughbuildingGraphKeyswith the same query shapes as the modules'useBuildingGraphhooks (so both observe the same react-query cache entries), and emits ids only —{},{storeyId},{storeyId, spaceId}or all three, spread-safe into create payloads. The denormalizedlocationNamelabel is a write-once historical value by decision; modules that persist one compose it at submit time from data the picker already fetched.What each donor contributed: CAFM's client-side space search above a threshold (
spaceSearchThreshold, default 8) and its element formatting hook (formatElement); service-desk's graceful error branch, ids-only emission semantics anddata-testidcontract (location-picker,-no-building,-unavailable, plus new per-select ids).Integration stays on the module's side of the boundary, which is why the three new peer dependencies are optional (
peerDependenciesMeta):@tanstack/react-queryis imported only by this component;@tv/uiis never imported at all — it resolves only through the authenticated registry and this repo's CI installs without credentials, so the picker takesInput/Labelthrough a structurally-typedcomponentsslot and renders native elements otherwise;react-i18nextstays entirely in the host — every user-facing string arrives via the requiredlabelsprop, no namespace is baked in.
[1.4.1] — 2026-08-19
Changed
- Vendored graph types re-synced with upstream. tv-building-graph#1 aligned that package with
schema.prisma— nullableBuildingChangeset.buildingId, the newsiteId, and optional provenance columns (externalId,sourceSystem,classifications) onStorey/Space/Element— landing upstream what this SDK's wire layer had been declaring on its own since 1.4.0. The vendored copy now matches upstreammainbyte-for-byte below its provenance header, and the wire layer derives instead of overriding: thebuildingId/siteIdoverrides onChangesetare gone, andProvenanceFieldsisRequired<Pick<…>>of the vendoredStoreyrather than a hand-maintained parallel declaration. Provenance stays non-optional on the wire — upstream made the columns optional for construction-side ergonomics, but API responses always include them, and the SDK keeps that guarantee. No public type changes: every exported wire shape is structurally identical to 1.4.0.
[1.4.0] — 2026-08-19
Added
Typed building-graph client. New subpath
@tv/extension-sdk/graphexportscreateBuildingGraphClient(): read + write for the core graph (storeys, spaces, elements) and its versioning surface (changesets with the full draft → submit → review → apply → revert lifecycle, point-in-time history and diffs, named snapshots), plus both catalogs. Every path, query parameter, request body and response shape mirrors tv-api's controllers and DTOs, so a consumer that typechecks is calling endpoints that exist — this is ADR 094 phase 2.The wire types derive from the
@tv/building-graphpackage (vendored, the way the events catalog and the OpenAPI/permissions snapshots already ship in this package —@tv/*resolves only through the authenticated registry, and this repo's CI must keep installing without credentials) through aSerialized<T>transform: over JSON, everyDateis an ISO-8601 string. Response-only realities the package does not model — nested includes, pagination envelopes, provenance columns (externalId,sourceSystem,classifications), the nullablebuildingId+siteIdon site-level changesets — are layered on top, verified againstschema.prisma.Why this exists: the fleet survey behind ADR 094 counted six hand-rolled
Spacetypes and two copy-pasteduseBuildingGraph.tshooks (service-desk, CAFM) drifting against the same six endpoints, and the shell's Structure page added a third client the same week. One typed client, three deletions.The client is transport-agnostic — it runs over any
PlatformApiClient(the context-provided client inside a federated module, or the shell's own). For the axios instances every current consumer actually has,platformApiFromAxios()wraps one in five lines, andapiPrefix: ''accommodates base URLs that already end in/api/v1. Also exported:buildingGraphKeys, a canonical react-query key factory (plain arrays, no react-query dependency) whose root segment matches the module hooks it replaces, so their caches survive the migration.
[1.3.1] — 2026-08-18
Fixed
Every subpath is now loadable from CommonJS. The
exportsmap declared onlyimportandtypeson eleven of its twelve subpaths —.,./manifest,./context,./events,./api,./heartbeat,./react,./testing,./version-check,./permissionsand./i18n. Only./nestjsdeclaredrequire. Each of the eleven now declares it too, resolving to the same file asimport.This failed in the one way that survives every gate. TypeScript resolves a CommonJS import of
./i18nby walking the conditions, finding norequire, and matchingtypesinstead — sotscreports success and the image builds clean. Node has no concept of atypescondition, finds nothing it can load, and throwsERR_PACKAGE_PATH_NOT_EXPORTEDthe first time the code actually runs. Compile-time green, runtime dead.It matters now because of who consumes this package. All twenty-five module backends are CommonJS — nineteen on
module: commonjs, six onnodenextwith no"type": "module"— so every one of them resolves this package under therequirecondition. None imports./i18nyet, which is the only reason 1.3.0 shipped without anyone noticing; the i18n migration currently moving through the fleet is what would have found it, one module at a time, in whatever environment ran the code first.The package stays
"type": "module"and ships one build:requireandimportpoint at the same ESM file, which Node loads throughrequire(esm). That is what./nestjshas always done, and the module images run Node 22, where it is unflagged. Nothing about the ESM entry points changes, so bundled frontend consumers resolving underimportare unaffected.A contract test now asserts that every subpath declares
requireandtypes, and thatrequireandimportagree — the check that would have caught this when./i18nwas added in 1.3.0.
[1.3.0] — 2026-08-09
Added
Locale resolution is now a supported API. New subpath
@tv/extension-sdk/i18nexportsresolveLocale()plus the storage keys the platform agrees on (LOCALE_STORAGE_KEY,RESOLVED_LOCALE_STORAGE_KEY,LEGACY_LOCALE_KEYS,LOCALE_LOOKUP_ORDER,FALLBACK_LOCALE). A federated module can follow the shell's language without writing its own detector.This closes the same class of gap the building-selection API closed in 1.2.0. Five modules — service-desk, leases, comfort, parking, room-booking — had each hand-written a
detectLanguage()that reads the shell'slocalStoragekey, copied from one another and already drifting. A module reading the wrong key does not fail loudly; it simply stays in the old language while the shell switches, which is how service-desk sat in Russian inside an English shell without anyone noticing. Thirty-three copies of a policy is thirty-three pull requests per change, so the policy moves here and a change becomes a version bump.Modules never ask the browser.
resolveLocale()reads storage and nothing else. A remote that callednavigator.languageitself could disagree with the shell it renders inside — an English panel on a Russian page — and neither half would look wrong in isolation. One detector, in the shell; everyone else follows it.Two keys, deliberately.
tv-platform-localemeans the user explicitly picked a language;tv-platform-locale-resolvedmeans this is what the shell is currently rendering, detection included. i18next's defaultcaches: ['localStorage']collapses the two — the first visit persists a detected language as though it had been chosen, and the browser's language is then ignored forever after. Keeping them apart is what lets an explicit choice outrank detection permanently while detection still reaches modules.Region tags fold only when they have to:
en-USresolves to anencatalogue, whilept-BRstayspt-BRrather than collapsing into aptcatalogue nobody ships. A stored language the module has no catalogue for is rejected rather than rendered, becausefallbackLngwould otherwise fill every key from the fallback catalogue and produce a fluent-looking page in the wrong language. Storage that throws — private mode, embedded webviews, locked-down kiosks — yields the fallback instead of taking down the federated route over a rendering preference.
[1.2.0] — 2026-08-04
Added
Building selection is now a supported API.
@tv/extension-sdk/contextgainsreadBuildingSelection(),subscribeBuildingSelection()andisSameBuildingSelection();@tv/extension-sdk/reactgainsuseSelectedBuildingId()anduseBuildingSelection(). A federated module can now follow the Building OS header selector — including reacting to changes while mounted — without parsing the host's storage itself.This closes a gap that had every module solving it privately.
PlatformContext.buildingwas the documented answer, but it travels through React Context, and React Context does not cross a Module Federation boundary unless@tv/extension-sdkis a shared singleton in both the host and every remote — which it is not.usePlatformContext()therefore throws inside a federated module even though the shell renders<PlatformProvider>, so it had zero module consumers, while five modules (cafm, equipment, leases, service-desk, bim) had each hand-written a parser for the shell'sbuilding-storagekey and itstv:building-selection-changedevent. The SDK now owns that read: the storage key and event name become the SDK's problem rather than each module's, and the same call keeps working when the shell later moves to a shared-singleton context.BuildingSelectionEntry.organizationis optional and parsed all-or-nothing — a half-populated value is dropped rather than rendered as a confident owner label. Older shells persist only{id, name}; the field is here from the start so consumers can render the owning organization as soon as a host publishes it, without waiting on another SDK release. It matters wherever building names are not unique: a platform admin sees several organizations' buildings at once, and production has more than one set sharing a name.The new functions do what the hand-written copies did not: they de-duplicate notifications so a 2-second fallback poll no longer re-renders consumers on every tick, they recover the primary building when only the legacy scalar id was persisted, and they reject the literal string
"undefined"that reached the store in the wild.
Changed
- README no longer teaches
useBuilding()as the way to scope a federated module to a building — that example did not work in the environment it was written for. It now leads withuseSelectedBuildingId(), states that Building OS has exactly one building selector and modules must not ship a second, and keeps thePlatformContextexample scoped to code running inside the shell bundle.
[1.1.0] — 2026-08-01
Minor rather than patch: the fixes below are patch-level, but
--scopeadds a CLI flag and aScaffoldInputfield. Additive surface bumps the minor.The headline is that
manifest.schema.jsonis usable for the first time. Until now it was emitted in output mode and rejected 30 of the 31 manifests actually shipping, so$schemahad to be left out of module manifests entirely. On this version you can point$schemaat it and get editor validation that agrees withtv-sdk validate.
Fixed
check-exposesno longer reports a false "Missing canonical expose./Shell" on modules that are correct at runtime (#31). It readsvite.config.tsas text, and two static-analysis faults compounded: the entry regex matched only quoted keys, so a computed[`./${EXPOSE_COMPONENT}`]never registered; and theexposes: { … }block was sliced with a non-greedy/\{[\s\S]*?\}/that stopped inside${EXPOSE_COMPONENT}, dropping every later entry once one used a placeholder. Simple single-fileconst NAME = 'literal'declarations are now folded in before matching, the entry pattern accepts template literals and the bracketed computed form, and a brace-balanced scanner (skipping strings, template literals including nested${}, and comments) replaces the regex. Anything still unresolvable is reported rather than silently dropped, and a missing./Shellalongside unresolved keys is a warning, not an error.manifest.schema.jsonis now emitted in input mode. It was generated with Zod's defaultio: 'output', which describes a manifest after parsing — so every field carrying a.default()(sdkVersion,buildingTypes,capabilities,mcpTools,lifecycle, plusui.remoteEntry,ui.dashboardWidgets,ui.defaultLandingByRole,navigation[].roles,mcpTools[].permissions) was published asrequired. The artifact exists so consumers can point$schemaat it for editor validation, where that is exactly backwards: it demanded the fields the validator exists to supply. Measured against tv-platform, the published schema rejected 30 of 31 shipping manifests — includingtv-module-example, the reference implementation — whiletv-sdk validateaccepted all of them. The mismatch went unnoticed because the$schemaURL the docs advertised 404'd. The schema and the Zod validator now agree exactly; the 5 manifests that still fail do so under both, and are genuine manifest bugs. Regenerated. Newsrc/manifest/generate-schema.test.tslocks the invariant so a future emitter change can't silently reintroduce it.init-module/init-manifestscaffolder defaults corrected. All three were wrong for the external module authors the developer portal now routes to this command:sdkVersionwas the hardcoded literal"0.1.0", stale since before the 1.0 contract. Now stamped from the package's own version at scaffold time. The platform gates on this field when deciding whether it can run a module, so a stale value is not cosmetic.$schemawas"../../packages/tv-extension-sdk/manifest.schema.json", which only resolves from inside the tv-platform monorepo and dangled for everyone else. Now"./node_modules/@tv/extension-sdk/manifest.schema.json", which resolves for anyone who installed the SDK (pnpm symlinks the workspace package into the same path). Useful again now that the schema itself is input-mode.- The module id hardcoded Tango Vision's own
@tvscope, so a third-party module was scaffolded into a scope it must not publish under.
Added
--scope=<scope>oninit-manifestandinit-module(andScaffoldInput.scope), with or without the leading@:--scope=acmeproduces@acme/module-<slug>in both the manifestidand the generatedpackage.jsonname. Defaults to@tvfor first-party use — unchanged behaviour for existing callers.
Changed
init-module's "Next steps" output now sayspnpm install/pnpm dev. pnpm is the platform standard; the scaffolder was the one place still telling authors to use npm.
[1.0.6] — 2026-07-24
Fixed
- Manifest schema: event names may now contain underscores within dot-separated segments (
[a-z0-9_-], segment must still start with a letter or digit). The old pattern rejected the platform's own first-party catalog events —building.element.status_changed, and the newservice-desk.ticket.sla_warning/sla_breached— sotv-sdk validatefailed on any manifest that declared them (hit while updating tv-module-cafm's manifest). Module/capability ids and permission subjects keep the stricter kebab-case pattern; onlyevents.publishes[].name/events.subscribes[].nameare affected.manifest.schema.jsonregenerated. buildSubject()had the same gap: it threwInvalid eventTypefor snake_case event types, including the catalog's ownbuilding.element.status_changed. NATS subject tokens permit_, so the guard now allows it (wildcard/injection characters are still rejected).
[1.0.5] — 2026-07-19
Changed
permissions.snapshot.jsonregenerated from tv-api'sPermissionRegistry(#11) — adds thebuilding.modelsandbuilding.paymentssubjects. Additive only: no existing subject or grant was removed. This version bump is what actually ships the regenerated catalog to consumers — the snapshot was regenerated in place in 1.0.4 without a version bump, so the published1.0.4tarball still carried the pre-regen catalog and installs never saw the new subjects.
[1.0.4] — 2026-07-17
Fixed
tv-sdk write-versionnow reads theTV_APP_COMMITenv var when--commitis not given, before falling back togit rev-parse HEAD. The standard module Dockerfile declaresARG TV_APP_COMMIT+ENV TV_APP_COMMIT=$TV_APP_COMMITexpecting exactly this, but.gitis dockerignored, so every containerized build stamped"commit": "unknown"into version.json. Resolution order is now--commitflag →TV_APP_COMMITenv (non-empty) →git rev-parse HEAD→"unknown"— the same precedence building-os uses for its__TV_APP_COMMIT__build constant.
[1.0.0] — 2026-05-30
First stable release. The surface tagged in Phase 2.1 (see STABILITY.md) is now binding under semver:
@stableexports carry a 6-month deprecation guarantee;@experimentalexports may still change in any release.No breaking API changes from
0.2.0— the move to 1.0 is the commitment to the surface, not a reshaping of it. Existing consumers upgrade by bumping the version; review the@experimentallist below and decide which symbols to track changes on.
Added
- Stability tiers documented: every subpath barrel now carries a
@stableor@experimentalJSDoc tag. See STABILITY.md. @tv/extension-sdk/permissions— permission catalog extractor + types (extractCatalog,extractCatalogFromFile,PermissionCatalog,SubjectEntry). Phase 2.4.tv-sdk snapshot-permissionsCLI command — extracts the catalog from tv-api'sPERMISSION_ROLE_MAPsource. Phase 2.4.permissions.snapshot.jsonartifact ships with the package (17 subjects, 33 actions as of 2026-05-25).@tv/extension-sdk/sandbox—tv-sdk sandboxCLI subcommand family (create / list / connect / extend / delete / reset). Phase 1a.4.@experimental— surface may grow during Phase 1a rollout.
Changed
- Build moved off
zod-to-json-schema(Zod 3-only) onto Zod 4's built-inz.toJSONSchema(). Affectsmanifest.schema.json+tv-events.snapshot.jsonoutputs — same contract, normalized representation. CI'scheck-eventsself-consistency passes against the regenerated snapshot. tsconfig.jsonadds"types": ["node"]sonode:fs/process/ etc. resolve. Closes silent SDK build break that started 2026-05-01.z.record()call sites updated to Zod 4's two-arg form (z.record(z.string(), …)).manifest.schema.jsonregenerated under Zod 4's emitter — more strict (more required fields surfaced). Downstream consumers using$schemafor editor validation should re-validate their manifests.
Removed
zod-to-json-schemadependency (no longer used).
Stability tiers in this release
Already @stable (no churn expected within a major):
@tv/extension-sdk/manifest— schema + validator + scaffolders + checkExposes@tv/extension-sdk/context—PlatformContexttypes@tv/extension-sdk/events— catalog, envelope, subjects@tv/extension-sdk/api—PlatformApiClienttypes@tv/extension-sdk/react— provider + core hooks@tv/extension-sdk/nestjs—@ModuleCapability(),@RequiresLicense()decorators@tv/extension-sdk/testing—createMockPlatformContext@tv/extension-sdk/permissions— extractor + catalog types- CLI:
validate,check-exposes,init-manifest,init-module,write-version,snapshot-permissions - JSON artifacts:
manifest.schema.json,permissions.snapshot.json
@experimental (may change in any release until graduation; see STABILITY.md):
@tv/extension-sdk/heartbeat@tv/extension-sdk/version-check@tv/extension-sdk/sandbox(CLI subcommands)- Manifest fields:
PayloadContract,McpToolDeclaration.permissions - CLI:
check-events,check-api,check-pact,snapshot-events,snapshot-api - React:
useVersionCheck,<UpdatePrompt> - NestJS:
TvHeartbeatModule.register()
[0.2.0] — 2026-04 (historical reference)
Initial SDK consolidation. ModuleManifest schema + PlatformContext + React/NestJS integration. See git history for details.
Migration guide: 0.x → 1.0
When 1.0.0 ships (Phase 2.2), the surface above becomes binding under semver. There are no breaking API changes between current pre-1.0 and the planned 1.0 — only the addition of the stability tiers. Existing internal consumers should not need code changes; they should review the @experimental list above and decide which symbols to track changes on more carefully.