@mcp-abap-adt/interfaces
v50.0.0
Published
Shared interfaces for MCP ABAP ADT packages
Downloads
14,843
Maintainers
Readme
@mcp-abap-adt/interfaces
Deprecated facade since 45.0.0. The contracts live in
@mcp-abap-adt/interfaces-utils,-network,-authand-adt. This package re-exports them; every symbol is marked@deprecatedwith the package to import it from. Nothing was removed and no contract changed. The domain documentation below still describes these contracts.
Shared interfaces for MCP ABAP ADT packages.
This package provides all TypeScript interfaces used across the MCP ABAP ADT ecosystem, ensuring consistency and type safety across all packages.
This package exists to be the one import point for the contract: object
configs, client options, abapGit, the execution atoms, the runtime and service
contracts all live here rather than in
@mcp-abap-adt/adt-clients, so a consumer imports one package and has exactly
one seam to override at — a custom IAdtContentTypes, a connection that
implements IDeferredResponseConnection, and so on — without reaching into
the implementation package to do it. Concrete implementations (parsers,
request builders, the shipped IAdtContentTypes classes) stay in
adt-clients; this package never depends on it.
Architecture, and why
docs/architecture/ARCHITECTURE.md
describes the shape: what an answer is, the two axes a consumer decides on, how
contracts are composed rather than inherited, what each family holds, and where
the seam to an implementation runs. Read it first if you are about to implement
one of these contracts or replace one.
docs/architecture/DECISIONS.md records the
choices in this contract that could reasonably have gone the other way — what
was decided, what it was decided against, and what would change it. It is a
log: entries are marked where a later decision superseded them, rather than
rewritten, because the reasoning that lost is worth reading.
Read it before proposing a shape that looks obviously better: several of the entries exist because that shape was tried, and the reason it lost is written down. The contract is measured rather than inferred, states absence by omission rather than by negative types, and does not validate what SAP sends — each with the evidence that settled it.
Installation
npm install @mcp-abap-adt/interfaces-adt # or -auth, -network, -utils: only what you accept
npm install @mcp-abap-adt/interfaces # deprecated facade: everything, as beforeOverview
This package contains all interfaces organized by domain:
adt/- ADT object operations interfaces (capability atoms, operation options, error codes), plus the abapGit client contract, ADT client options, and the content-type/header contractauth/- Core authentication interfaces: configs and auth types, the credential contract a connection authenticates with (IAuthProvider), and how an interactive login is conducted (IAuthorizationStrategy)token/- Token-related interfaces (token provider, results, options)session/- Session storage interfaceserviceKey/- Service key storage interfaceconnection/- Connection and realtime transport interfaces (AbapConnection, request options, WebSocket transport contracts, deferred-response detection)execution/- Execution contracts for runnable entities (IAdtRunnable, the two profiler atoms, class/program executors composed from them)feeds/- Feed access interfaces (IFeedRepository, feed entries, system messages, gateway errors)runtime/- Runtime analysis domain interfaces (profiler, traces, dumps, logs, ATC, system messages, gateway errors)sap/- SAP-specific configuration (SapConfig, SapAuthType)service/- Business service lifecycle contracts (parameters and variants; service binding params)storage/- Storage interfaces (session storage, state)logging/- Logging interfaces (ILogger, LogLevel enum)validation/- Validation interfacesutils/- Utility types and interfaces
Interface Naming Convention
All interfaces start with I prefix (e.g., IAbapConnection, ISapConfig, ITokenProvider).
This ensures consistency across all packages and follows TypeScript naming conventions for interfaces.
Usage
Basic Imports
import {
IAuthorizationConfig,
IAuthProvider,
IConnectionConfig,
ISessionStore,
IServiceKeyStore,
ITokenProvider,
IAbapConnection,
IAdtRunnable,
IWebSocketTransport,
IWebSocketMessageEnvelope,
ISapConfig,
ILogger,
TOKEN_PROVIDER_ERROR_CODES,
STORE_ERROR_CODES
} from '@mcp-abap-adt/interfaces';Writing Your Own Credential
IAuthProvider is here, rather than beside any one implementation, so that an
authentication nothing ships can still be used. A credential states all of itself: kind,
prepare(), authorizationHeader(), cookies() and transportMaterial(), each empty
where there is nothing to say. That is the whole of it — there is nothing further to add
and nothing to declare.
import type {
IAuthProvider,
ICertificateMaterial,
} from '@mcp-abap-adt/interfaces';
class HeaderTokenProvider implements IAuthProvider {
readonly kind = 'my-gateway-token';
constructor(private readonly token: string) {}
// Empty where there is nothing to say, which is most of a header credential.
async prepare(): Promise<void> {}
cookies(): string | null {
return null;
}
transportMaterial(): ICertificateMaterial {
return {};
}
async authorizationHeader(): Promise<string | null> {
return `Bearer ${this.token}`;
}
}A credential that authenticates through TLS has no header at all, and says so with
null rather than an empty string:
import type {
IAuthProvider,
ICertificateMaterial,
} from '@mcp-abap-adt/interfaces';
class PfxProvider implements IAuthProvider {
readonly kind = 'pfx';
constructor(private readonly pfx: Buffer, private readonly passphrase: string) {}
async prepare(): Promise<void> {}
cookies(): string | null {
return null;
}
async authorizationHeader(): Promise<string | null> {
return null;
}
transportMaterial(): ICertificateMaterial {
return { pfx: this.pfx, passphrase: this.passphrase };
}
}A credential that may only be presented once has no home here yet. SPNEGO is the case —
its token is consumed by the request that carries it — and this package shipped two contracts
for it, ICredentialOwningItsFetch and ICredentialTransport, which nothing ever
implemented. They were removed in 21.0.0.
They are not replaced by "answer with the token once and null afterwards", which looks
right and is not: a wire asks authorizationHeader() per attempt and retries a failed
establishment, so a credential that marked itself spent when the header was handed out would
send nothing at all on the second attempt — after a timeout, an aborted connection, or a
refusal that never reached the server. It has no way to know whether the request it was asked
for went out, let alone succeeded.
So the problem is open, and it is a real one: such a credential needs either an exchange it owns end to end, or a signal that the establishing request succeeded. Whoever adds SPNEGO decides which — from that requirement, rather than from a contract written before anything needed it.
ADT Object Operations
import {
IAdtReadable,
IAdtOperationOptions,
AdtObjectErrorCodes,
LogLevel
} from '@mcp-abap-adt/interfaces';
// Example: Read with long polling.
// A domain is `IAdtMetadataReadable` and nothing else — it has no source, so
// `readMetadata` is the whole of reading it. A class, which has both, would use
// `read` here for its source and `readMetadata` for its own document.
const domain = await adtDomain.readMetadata(
{ domainName: 'Z_TEST' },
{ withLongPolling: true } // Wait until object is available
);
// Example: Read metadata with long polling and version selection
const metadata = await adtDomain.readMetadata(
{ domainName: 'Z_TEST' },
{ withLongPolling: true, version: 'active' }
);Error Handling
import {
TOKEN_PROVIDER_ERROR_CODES,
STORE_ERROR_CODES
} from '@mcp-abap-adt/interfaces';
// Token Provider Error Codes
try {
await tokenProvider.getTokens(authConfig);
} catch (error: any) {
if (error.code === TOKEN_PROVIDER_ERROR_CODES.VALIDATION_ERROR) {
console.error('Invalid auth config:', error.missingFields);
} else if (error.code === TOKEN_PROVIDER_ERROR_CODES.REFRESH_ERROR) {
console.error('Token refresh failed:', error.cause);
}
}
// Store Error Codes
try {
const authConfig = await serviceKeyStore.getAuthorizationConfig('TRIAL');
} catch (error: any) {
if (error.code === STORE_ERROR_CODES.FILE_NOT_FOUND) {
console.error('Service key not found:', error.filePath);
} else if (error.code === STORE_ERROR_CODES.PARSE_ERROR) {
console.error('Invalid JSON:', error.filePath, error.cause);
} else if (error.code === STORE_ERROR_CODES.INVALID_CONFIG) {
console.error('Missing fields:', error.missingFields);
}
}Responsibilities and Design Principles
Core Development Principle
Interface-Only Communication: This package defines contracts — interfaces, types and the constants they refer to. It contains no implementations: no classes, no functions, and no dependency on any implementation or runtime package — only on its four sibling contract packages. It is the single source of truth for the shapes every package here agrees on.
Package Responsibilities
This package is responsible for:
- Defining interfaces: Provides all TypeScript interfaces used across MCP ABAP ADT packages
- Type safety: Ensures consistent type definitions across all packages
- Version management: Single version for all interfaces
- Documentation: Centralized documentation for all interfaces
What This Package Does
- Defines interfaces: All interfaces used across MCP ABAP ADT packages
- Organizes by domain: Interfaces grouped by functional domain
- Follows naming convention: All interfaces start with
Iprefix - Contracts, not code: types and interfaces, plus the constants they name. Since 29.0.0 the package ships no class and no function —
AdtOperationError,TransportSearchConfigurationMissing,isNetworkError()andhasDeferredResponses()were removed, because a contract says what a thing is and shipping one way of being it makes "use your own implementation" untrue for that piece. What remains executable is 51 exports: 43 string constants (theHEADER_*andAUTH_TYPE_*names, plusADT_NO_FAILURE), 6 maps of codes (AdtObjectErrorCodes,NETWORK_ERROR_CODES,SERVICE_BINDING_VARIANT_MAPand three more) and two enums (AuthMethodPriority,LogLevel); every emitted module is otherwise empty
What This Package Does NOT Do
- Does NOT implement anything: no class and no function is exported. The only executable output is the constants listed above, which are values a contract names rather than behaviour it performs
- Does NOT have implementation or runtime dependencies: only on its four sibling contract packages, plus devDependencies for TypeScript compilation
- Does NOT know about implementations: Interfaces are independent of implementations
Interface Domains
ADT Domain (adt/)
Capability atoms (
adt/IAdtCapabilities.ts, since 11.2.0) — one small interface per operation, and nothing above them. A handler declares the atoms it honours, so a consumer reading it learns what that object can do and what comes back:- Since 29.0.0 these members answer
Promise<IAdtResponse<TValue>>instead of throwing, and each atom names what its own member returns — a create does not answer what a read answers, and one type for all of them said something untrue about ADT. Since 30.0.0 nothing in this package throws:lock,unlock,getVersionsandgetVersionSourcewere the last four exempt, on the grounds that they have no failure half, and a lock refused because another user holds it is a 403 IAdtCreatable<TConfig, TCreated>—createIAdtReadable<TConfig, TSource>—read. The object's source at its ownsource/mainIAdtMetadataReadable<TConfig, TMetadata>—readMetadata. The object's own document. Split fromIAdtReadablein 36.0.0: one atom demanding both members made eight types answerreadandreadMetadatawith the identical request, which is one endpoint behind two members. Its own JSDoc had admitted it — "for objects without source code this returns metadata XML" — and a domain now composes this atom alone, which is the statementIAdtUpdatable<TConfig, TUpdated>—update. Writes the source (since 15.0.0)IAdtMetadataUpdatable<TConfig, TMetadataUpdated>—updateMetadata. Writes the object's own document (36.0.0). The same split as reading, for the same reason: a member is named for the resource it addresses, so a caller never has to know which kind of object it holds to know whatupdatewill write. Three types compose both — a function include, a scalar function implementation and a feature toggle each have two writable resources — and eight compose only this oneIAdtDeletable<TConfig, TDeleted, TChecked>—delete,checkDeletion(since 15.0.0; the check joined it in 35.0.0). Two members because they are two requests, one atom because they are one operation: almost everything created can be removed, and what varies is the moment — something still references it, a transport holds it, another user holds its lock. Only the server knows, so anything that can be deleted can be asked whether it can be deleted nowIAdtValidatable<TConfig, TValidated>—validateIAdtCheckable<TConfig, TChecked>—checkIAdtActivatable<TConfig, TActivated>—activateIAdtLockable—lock,unlockIAdtVersionable—getVersions,getVersionSourceIAdtTransportAware<TConfig, TTransport>—readTransportIAdtRequest<TList>(adt/IAdtTransport.ts, since 26.1.0) — what the transport request has that nothing else does:list(). It exists becauseAdtClient.getRequest()returned a concrete class, and a concrete return is the one a consumer cannot replace, cannot compose their own types into, and cannot check a capability claim against. Since 30.0.0 it extends nothing — a caller who also creates transports writesIAdtRequest & IAdtCreatable<ITransportConfig, string>— andlistNodes()is gone: it andlist()answered the identical tree from one requestIAdtTransportObjectActions<TRemoved, TAdded, TTask, TActionLog, TObjects>(adt/IAdtTransport.ts, since 45.1.0; fifth parameter andreadObjectssince 46.0.0) — what can be done to a request's object list and to its tasks:removeObject,addObject,createTask,readActionLog,readObjects. A listing already answers everyatom:linka request and its tasks carry —release,addobject,changeowner,newtask— so that a caller follows an href rather than assembling a URL; handing over the addresses of operations while declaring nothing that performs one leaves the caller buildingtm:rootdocuments by hand. Five members and no composite: the order they are called in, and what to do whenaddObjectis refused, is the caller's. Two of the signatures changed in 46.0.0 because the first run against a server refuted them —removeObjectrequirespositionor the server answers200and removes nothing,createTaskrequirestargetUseror it is refused with an empty user name — andreadObjectsexists so the position has somewhere to come fromIAdtRunnable<TTarget, TResult, TOptions>(execution/IAdtRunnable.ts, since 16.0.0) — the capability of being executed, one method. The profiler atoms are composed beside it where a runner also profiles, and a unit-test handler declares it alone: there is no test-specific runnable, because two differently-shaped contracts for "this can be executed" would be two vocabularies for one idea.searchis not an object atom.IAdtSearchablewas removed in 30.0.0: searching is not something an object does to itself, and the question already had a home inIAdtInformationSystem.search. Declaring it in both places made one endpoint two members across two files. Since 40.0.0 that member has its own atom,IAdtObjectSearch, on the information system where it always lived — the split is between endpoints of the information system, not between objects, and the name is different because the idea is.ITestRunInformationandICdsTestDoubleCheckable(adt/IAdtUnitTest.ts, since 16.0.0) — asking about a run by its id, and asking whether a CDS view can be tested with doubles. Both were part ofIAdtTestRunnableuntil 16.0.0; neither is running.- Since 29.0.0 there is no composite at all.
IAdtObject,IAdtCrud,IAdtModifiableandIAdtSourceObjectwere removed: they forced one result type on members that answer different things.src/__typechecks__/capabilityAtoms.tsproves what replaces them — each atom is independently satisfiable, one cannot stand in for another, andIAdtCreatable<Config, string>is notIAdtCreatable<Config, void>. - The grain follows ADT, not taste:
lock/unlock,getVersions/getVersionSourceandcheckDeletion/deleteare honoured or refused as pairs, because each is one operation seen from two ends.updateanddeletewere taken for a third such pair until 15.0.0 and are separate atoms since — nothing in ADT ties changing an object to removing it, and a handler that supports one can now say so without claiming the other. - Since 17.0.0 no interface in this package declares a capability the object does not have, and since 30.0.0 no contract extends another at all (decision 23).
IFeatureToggleObjectno longer inherits the atoms it satisfies: a consumer spells the composition they need, so an implementation that only switches a toggle is a legitimate one instead of owing eight members it does not have.IAdtServiceBindingwent further and is gone — a binding has no interface of its own at all, which is where that reasoning ends up when followed: if a consumer spells what they need, the aggregate has nothing left to do. That is asserted rather than believed: a guard in@mcp-abap-adt/adt-clientscompares all 37 factory return types against the 12 atoms in both directions, and calls every declared method to check it issues the request its capability names. - There is no atom for "everything but versions" — a capability vocabulary states what an object supports, never what it lacks. A handler that is the full set minus
IAdtVersionablelists the atoms it does honour (see the 15.0.0 CHANGELOG entry for why the earlierIAdtNonVersionedObjectcomposite was removed).
- Since 29.0.0 these members answer
IAdtOperationOptions- Unified options for create and update operations- Fields:
analyse,source,lockHandle,timeout— the error strategy, the body, and what goes on the request. The body is one field since 50.0.0: it wassourceCodeandxmlContent, split by whether the payload was ABAP text or an XML document, which asked the caller to classify something this package never reads (decision 32). Nothing about what a member should do after it:activateOnCreate,activateOnUpdateanddeleteOnFailurewere removed, because they asked the caller to compose the member out of steps, and steps are the implementation's.activateis a member; call it. analyse(since 29.0.0) is the caller's own reading of what counts as a failure:IAnalyse<E extends IAdtError = IAdtError>, that is(verdict: IAdtError | AdtNoFailure, answer?: IAdtWireResponse) => E | AdtNoFailure. Since 32.0.0 the failure type is the caller's: name it and it reachesgetError()without a cast, which is why the members takeIAdtOperationOptions<E>and why the parameterised call signature requires the strategy that earns it. Since 31.0.0 "this is not a failure" is the exported tokenADT_NO_FAILURE, notundefined— the field is optional, soundefinedalready meant "there is no strategy here", and one value cannot mean both that and a strategy's verdict of "fine". It is handed the default's verdict and the answer it was reached from, so it can overrule in either direction. It exists because no single reading serves every caller — ADT answers a request for a missing object with 200 and an empty body, and those same bytes are a failure to a read-modify-write, since writing back what it read erases the object, and an empty list to a listing
- Fields:
AdtObjectErrorCodes- the codes a failure names itself by, read fromgetError().code. No member of this contract throws since 30.0.0 — which is about what the server's answer becomes, not a ban on exceptions inside an implementation: what goes wrong while a library reads an answer is that library's own- Constants:
OBJECT_NOT_FOUND,OBJECT_NOT_READY,VALIDATION_FAILED,CREATE_FAILED,UPDATE_FAILED,DELETE_FAILED,ACTIVATE_FAILED,CHECK_FAILED,LOCK_FAILED,UNLOCK_FAILED
- Constants:
No state types.
IAdtObjectStateand the 31 per-objectI<Object>Stateinterfaces were removed in 29.0.0. They were ten optionalIAdtWireResponsefields, nineundefinedon any given call, from which a caller could type nothing out. A member now answers what its own endpoint produced, andIAdtError.requestnames the step that refused — which is what the bags were nominally for. A state is a shape an implementation builds:@mcp-abap-adt/adt-clientsdeclares its ownIAdtObjectConfig- Base configuration interface for ADT objects- Common fields:
packageName,description,transportRequest
- Common fields:
Per-object-type contract types (
IAdt<Object>.ts, one file per ADT object type — class, program, interface, table, domain, dataElement, ddl, structure, package, functionGroup/Module/Include, behaviorDefinition/Implementation, metadataExtension, enhancement, accessControl, serviceDefinition/Binding, transformation, scalarFunction(Implementation), tableType, appendStructure, authorizationField, featureToggle, messageClass, transport, unitTest):- Low-level operation params —
ICreate/IRead/IUpdate/IDelete<Object>Params(snake_case where the object uses it; some fields are camelCase, e.g.masterSystem/masterLanguage, matching the client) - High-level
I<Object>Config— theTConfigof every atom. The matchingI<Object>Statetypes are gone; what a member answers is named per atom - Object-specific enums and the pieces a request is built from (e.g.
EnhancementType,ServiceBindingVariant,IFixedValuefor a domain's fixed values,IStructureField, CDS/class-includes configs). The result helpers that used to sit beside them left in 31.0.0 — behaviorDefinition'sICheckRunResult/IValidationResult/ILockResult, message-classIParsedMessageClass/IParsedMessage,IEnhancementMetadata— each a shape nothing in the contract answered, and so the implementation's to declare - This package is the single definition site for these;
@mcp-abap-adt/adt-clientsimports and re-exports them (its public API is unchanged).
- Low-level operation params —
Cross-cutting shared types (
adt/IAdtShared.ts) —AdtObjectType(+lower/source variants),IObjectReference(what a group operation is given, per object), and the request parameters for search, where-used, virtual folders, SQL, table contents and discovery. The result shapes left in 31.0.0 —ISearchResult,IWhereUsedListResult,IPackageContentItem,IPackageHierarchyNode,IInactiveObjectsResponse,IAdtObjectHitand the rest — because what a reading builds out of a document is the implementation's (decision 24).IObjectReferencestayed and now states its own fields: a caller cannot callactivateObjectsGroupwithout it.IAdtObjectHitwas the common base of everything the repository handed back as a located object, and left with them in 31.0.0. What a hit is — a name plus an ADT type code — is now stated by whichever implementation answers one, and 13.0.0's point survives inIObjectReference: the field istype, notadtType, and it means the same thing wherever it appears.
Cross-cutting operations (
adt/IAdtUtilities.ts, since 26.2.0) — atoms for the operations that are not per-object CRUD, split by the resource families ADT itself has:IAdtInformationSystem(/repository/informationsystem/*, a composite ofIAdtObjectSearch,IAdtWhereUsed,IAdtVirtualFoldersandIAdtTypeCataloguesince 40.0.0),IAdtRepositoryStructure(/repository/nodestructure,/objectstructure),IAdtGroupLifecycle(/activation,/deletion),IAdtDataPreview(/datapreview/*),IAdtDiscovery(/discovery) andIAdtObjectAccess(the per-type resources reached generically, by type and name — what a caller uses when the type is a value rather than a decision). The result shapes those atoms answered left in 31.0.0 —IRepositoryObjectNode,IRepositoryNodeContentsandIRepositoryNodeChildamong them — and each atom names its reading as a type parameter instead. Since 44.0.0 that holds for every member that makes a request: thirteen of them still pinned their result tostring, which is the contract choosing the document and no injected reading being able to change it. The three that stay unparameterised —modifyWhereUsedScope,supportsSourceCode,getObjectSourceUri— issue no request, and a reading applies to an answer. What 27.0.0 established is not lost by that: a walk needs each object type paired with the node id holding it, and an implementation that answers ids alone cannot answer "which node holds the includes" — it is now the implementation's contract to keep, andadt-clientskeeps it.- Packages had their own atom until 43.0.0, on the reasoning that asking what is in a package is a question about the container rather than about the resource walked to answer it. The reasoning held; the member did not. Answering it takes one node-structure request per object type plus a descent into subpackages, and a member that walks can never be given a reading.
fetchNodeStructureanswers one level, and the caller walks. - The split is architectural, not observational, and the removal of six uncalled members turned that from reasoning into evidence. The one legacy implementation refuses every member of
IAdtDataPreview—getSqlQuery,getTableColumnsandgetTableContents: a whole family, refused whole. When the split was chosen it refused three,getTransactionamong them, so a split drawn along refusals would have given three atoms and a bag of twenty-eight, and one of those atoms would have evaporated when its member did. Refusals fall inside these families rather than defining them; a contract split by who refuses what changes shape with the next system. - Every member states a result since 30.0.0. Where the contract names a parsed shape, the interface carries a type parameter for it, so a consumer who needs the document — or their own shape — supplies an
IResultStrategywhen they construct the implementation, and the member's result type follows (decision 22). - Six members were removed before shipping because nothing anywhere called them, and three more as envelope leaks whose contract-shaped sibling was already beside them — see the 26.2.0 CHANGELOG entry.
searchtakes no parser since 30.0.0, and neither does anything else.IAdtObjectSearch<TSearch>carries the reading instead: a consumer who needs the document —mcp-abap-adthands the search XML to a language model — constructs an implementation with that strategy and callssearch(criteria). A per-call parser was a second signature every implementer owed whether or not their callers used it, and it moved the result's meaning from the contract to the call site (decision 22).- The package walk left in 43.0.0, and
IAdtPackageBrowsingwith it.getPackageContentswas a walk on the wire — one node-structure request per object type, plus a descent into subpackages — andIResultStrategytakes a single answer, so no reading could ever be given for it.fetchNodeStructureinIAdtRepositoryStructureis the step it was built from: one request, one reading, one level, and the caller walks.getIncludesList,listFunctionModulesandlistFunctionGroupIncludesleftIAdtObjectAccessin 41.0.0 for the same reason. AdtClient.getUtils()in@mcp-abap-adt/adt-clientsstill returns the concreteAdtUtils; these atoms are what it will return once that package consumes them.
- Packages had their own atom until 43.0.0, on the reasoning that asking what is in a package is a question about the container rather than about the resource walked to answer it. The reasoning held; the member did not. Answering it takes one node-structure request per object type plus a descent into subpackages, and a member that walks can never be given a reading.
abapGit client contract (
adt/IAdtAbapGit.ts, since 14.0.0) —IAdtAbapGitClient(link, pull, unlink, listRepos, getRepo, getErrorLog, checkExternalRepo) plus the arguments a caller needs to invoke them (IAbapGitLinkArgs,IAbapGitPullArgs,IAbapGitUnlinkArgs,IAbapGitExternalRepoCredentials,IAdtAbapGitClientOptions). The result shapes left in 31.0.0 —IAbapGitRepoStatus,IAbapGitPullResult,IAbapGitErrorLogEntry,AbapGitStatusand the external-repo shapes — and arrive as five type parameters instead;IAbapGitPullArgsis not generic since 43.0.0:pullis one POST to thepullLinka caller passes, so the four fields that configured a wait —pollIntervalMs,maxPollDurationMs,signalandonProgress— went with the loop they parameterised. A caller lists the repositories once, keeps the link, posts, then pollsgetRepoon their own terms and readsgetErrorLogif the status says to. Moved verbatim from@mcp-abap-adt/adt-clients'AdtAbapGitClient, which still owns the implementation.ADT client options (
adt/IAdtClientOptions.ts, since 14.0.0) —IAdtClientOptions(enableAcceptCorrection,masterSystem,responsible,masterLanguage,contentTypes,unicode) andIAdtSystemContext, so configuring a client does not require importingadt-clientsto describe the options.Content-type contract (
adt/IAdtContentTypes.ts, since 14.0.0) —IAdtHeaders(accept,contentType) andIAdtContentTypes, the per-operation Accept/Content-Type provider a consumer overrides for a system that needs different headers. The two shipped implementations (AdtContentTypesBase/AdtContentTypesModern, 354 lines/38 methods) andresolveContentTypes()stay inadt-clients— that is behaviour, not contract.Standalone
PROG/Iincludes (adt/IAdtInclude.ts, since 22.0.0) —IIncludeConfig,ICreateIncludeParams,IUpdateIncludeSourceParams,IDeleteIncludeParams, plusIAdtContentTypes.includeCreate(). An include is a different resource from a program, measured: it answers withinclude:abapInclude, its own namespace,adtcore:type="PROG/I"andinclude:contextRefCount, against a program'sprogram:abapProgram,program:programTypeandPROG/P— and the two collections advertise different accepted content types, so modelling one as a flavour of the other builds the wrong document and posts it to the wrong place. There is noIValidateIncludeParams:/includes/validationtakes the same three parameters/programs/validationdoes. Creation is a modern on-prem capability — only there does discovery give the includes collection anapp:accept, and a collection without one is not a POST target.Transport search configuration (
adt/IAdtTransport.ts) —IListTransportsParams.configUriis required (since 14.0.0, breaking): the five filter fields it replaces (user,status,date_range,target_system,request_type) were never read by the server —/sap/bc/adt/cts/transportrequestsis a saved-configuration search, not a filtered query.IListTransportsOptions(configUrioptional) is the high-level surface that opts into resolving a default configuration.ITransportSearchConfigurationdescribes one saved configuration (uri,etag,attributes);TRANSPORT_SEARCH_CONFIGURATIONS_URLis where they live. The error raised when none exists belongs to the implementation —@mcp-abap-adt/adt-clientsexports it — because this package ships contracts, not classes. See the 14.0.0 CHANGELOG entry for the migration and the probe evidence.A request's object list (
adt/IAdtTransport.ts, since 45.1.0) —IAbapObjectEntryis one entry as the CTS object directory holds it:pgmid,type,name, and optionally the description and the position within a task.positionis optional on the entry and required byremoveObject, which is not a contradiction: an entry being described is not an entry being addressed, andaddObjecthas no position to give because the entry does not exist yet.readObjects(since 46.0.0) is where a position comes from — the reading that lists what a request or task holds. It is notIObjectReference, though they look alike: that one is ADT's vocabulary, wheretypeis an object type code such asCLAS/OCand auriandparentNamecome with it, while the object directory speaks a program id, a short type and a name —R3TR FUGR ZMCP_BLD_FGR_H1. Merging them would give a type where half the fields are always wrong andtypemeans one thing or the other depending on which member was called. It is here for the reasonIObjectReferenceitself is: a request parameter a consumer cannot call a member without. Why the members exist at all is measured — deleting an ABAP object does not free its name, because the object-directory entry stays on the request that carried it, and until it is detached a create of the same name is refused withCTS_WBO_API 019even when that same request is passed ascorrNrThe transport tree left in 31.0.0.
ITransportTreeand its nodes were what our parser built out of the/cts/transportrequestsdocument.IAdtRequest<TList>names the reading as a type parameter instead, so an implementation says what it answers and a consumer parsing differently is not arguing with a shape declared here.
Authentication Domain (auth/)
IAuthorizationConfig- Authorization values (UAA credentials, refresh token)IConnectionConfig- Connection values (service URL, token, client, language)IConfig- Composition of authorization and connection configAuthType- Auth type:'jwt' | 'xsuaa' | 'basic'ICallbackServerOptions/ICallbackServerHandle/CallbackServerFactory- Lifetime contract for the local listener that receives an interactive login's redirect. The handle is borrowed inside a factory callback and the port is released on the first terminal outcome — the callback returning or throwing, an explicit failure, the timeout, or an abort — so releasing the socket is never a consequence of a wait settling.timeoutMsis mandatory and cancellation is available through anAbortSignal.portaccepts0to bind an ephemeral port (since 11.6.0), in which case the authorization URL must be built fromhandle.redirectUri; a flow that assembles its URL before binding, or one whose redirect is registered with the identity provider such as a SAML ACS, cannot use it.loggeris where the transport reports an ignored request.IAuthorizationStrategy<TResult>/AuthorizationRequest/AuthorizationOutcome<TResult>(since 11.6.0) - How an interactive authorization is conducted, so a consumer can supply its own instead of the shipped one.AuthorizationRequest.buildAuthorizationUrl(redirectUri)is async and is called once the strategy has settled on its redirect URI — which is what makes an ephemeral port possible, since the URL cannot be assembled before the socket is bound.authorize()resolves with anAuthorizationOutcomethat carries the redirect URI alongside the payload, because the token exchange must send that same URI and, with an ephemeral port, has no other way to learn it.ICertificateMaterial/ICertificateMaterialLoader- Loaded TLS client-certificate material (cert/key/pfx/passphrase) and the loader that produces it from a config. Structural on purpose: it is the shape an HTTPS client needs, named without importing one — this package depends on no HTTPS implementation and ships no code that would use one.IAuthProvider(since 17.2.0) - How a connection proves who it is on each request, as opposed to which system it is dialling. Deliberately not "give me a token": four of the five ways in are not tokens — basic is a header built from a username, a certificate is TLS material and no header at all, SPNEGO is a negotiation with the server. Since 20.0.0 a credential states ALL of itself:kind,prepare(),authorizationHeader(),cookies()andtransportMaterial()are required, and each is empty where there is nothing to say — "nothing to prepare", "I am not cookies", "I contribute no TLS material". Those are facts, and a fact is stated rather than left for a caller to discover by checking whether a method exists.IRenewableCredential(since 19.0.0) is the atom for the one that used to sit among them:renew(), "the server refused what you last handed out, get a new one". Only some credentials have it — a password is a password, and a SAML session was negotiated elsewhere — so it is narrowed to rather than carried by all. Nothing in a request path should call it: renewal on an expiry the provider can see happens insideauthorizationHeader(), which is asked per request, and this is the other case, where deciding what a refusal MEANT belongs to the caller.It is an atom, so narrow to both halves. A guard answering
c is IRenewableCredentialhands the caller something that renews and cannot authenticate — the atom carriesrenew()and nothing else:function isRenewable(c: IAuthProvider): c is IAuthProvider & IRenewableCredential { return typeof (c as Partial<IRenewableCredential>).renew === 'function'; }That is the shape every atom here takes, and the mistake is easy to make because the older interface did include the provider.
authorizationHeader()answersstring | null—null, not'', because the empty string is a legal header value and a credential that authenticates through TLS genuinely has no header.transportMaterial()returnsICertificateMaterialfor those.A credential that may only be presented ONCE — SPNEGO, whose token is consumed by the request that carries it — has no home here yet.
ICredentialOwningItsFetchandICredentialTransportexisted for it and were removed in 21.0.0, having never been implemented; and they are not replaced by answering once andnullafterwards, becauseauthorizationHeader()is asked per ATTEMPT and a failed establishment is retried, so a credential that marked itself spent when the header was handed out would send nothing on the next attempt. Such a credential needs either an exchange it owns end to end, or a signal that the establishing request succeeded. See Writing Your Own Credential.Distinct from
IAuthorizationStrategyabove, which is one layer up: that is how an interactive login is conducted, asked once by a human, and its output eventually becomes a token some implementation of this hands out. This one is asked on every request.
Token Domain (token/)
ITokenProvider- Token provider interface (stateful token lifecycle)ITokenProviderOptions- Options for token providersITokenResult- Token result payload (supportsexpiresAtandtokenTypefor non-JWT tokens)IConnectionConfig/ISapConfig- now supportauthType: 'saml'andsessionCookiesITokenRefresher- Token refresher interface for DI into connections- Created by
AuthBroker.createTokenRefresher(destination) - Injected into
JwtAbapConnectionto enable automatic token refresh - Methods:
getTokens()
- Created by
Session Domain (session/)
ISessionStore- Session storage interface
Service Key Domain (serviceKey/)
IServiceKeyStore- Service key storage interface
Connection Domain (connection/)
IAbapConnection- Minimal connection interface for ADT operations- Consumer-facing methods:
connect(),getBaseUrl(),getSessionId(),setSessionType(),makeAdtRequest() connect()initializes the session (CSRF token + cookies) before any ADT requests- Implementation details (auth, CSRF, cookies, token refresh) are encapsulated
- For JWT: token refresh handled internally via
ITokenRefresher - For Basic: no token refresh needed
- Consumer-facing methods:
IAdtWireResponse- the transport frame returned bymakeAdtRequest():data,status,statusText,headers. NamedIAdtResponseuntil 28.0.0, which is where the trouble was — one type meant both "what came off the wire" and "what a caller gets"IAdtResponse<TValue, TError>(adt/IAdtResponse.ts, since 28.0.0; reshaped in 29.0.0) - what a member answers with, and a discriminated union:IAdtSuccess<TValue>(ok: true,getResult(): IAdtResult<TValue>) orIAdtFailure<TError>(ok: false,getError(): TError). It takes the value, not a wrapper around it: 28.0.0 constrained the first parameter toIAdtResult<unknown>, so every member wroteIAdtResponse<IAdtResult<X>>— two wrappers where one was meant — and the contract displayedunknownto anyone reading it.TErrordefaults toIAdtErrorand stays constrained to it, so an implementation answeringIAdtError & { retryAfter: number }can say so while a caller written againstIAdtErrorreads it unchanged. Each half declares only its own method since 31.0.0, so checkinganswer.okis not a convenience but the way in: callinggetError()on an unnarrowed response is a type error rather than a sentinelundefinedIAdtResult<T>(since 28.0.0) - the result half, and a contract like the error half:valueis what the member promised. The two halves vary differently, and that is not a slip — an error strategy varies the fullness ofIAdtError, which has two required fields and five optional; a result strategy variesTitself —IAdtInformationSystem<MyHits, …>answersMyHitsfrom the same call, and another implementation answers its own — because a hit shape with a requireddescriptioncannot be returned half-filled. WhatIAdtResultmust never hold is the transport frame —IAdtErrorkeeps aresponsebecause diagnosing a failure needs the status it arrived with, and reading a result does notIAdtError(since 28.0.0) - the contract every error strategy returns. A strategy chooses how much to fill in, never what it is:brief,mediumandfullare three amounts of one contract, so a caller writes against it once.origin('connection' | 'refusal'— what the server said, or the absence of an answer) andmessageare required.'parse'left in 31.0.0: an answer that arrived and could not be read is a failure inside an implementation, and a strategy is free to read any way it likes or not to parse at all, so the contract cannot name that step. What an implementation does when its own reading fails is its business, and it may throw;adtType,namespace,response,requestandcodeare what a fuller strategy adds.causeleft in 31.0.0 with'parse': it carried whatever the transport or a parser threw, and a thrown object from inside an implementation is that implementation's, not something the contract describes — a consumer could not read it type-safely anyway.message,responseandrequestare what a failure says about itself.code(since 30.0.0) carriesAdtObjectErrorCodes— it is there because the contract promises specific failures in specific places, such asUNSUPPORTED_OPERATIONfromgetVersionson a type with no version resource, and until those members stopped throwing a consumer read that code off whatever was caught. An implementation may fill it in however it likes and a consumer's code does not change, because the methods are the same- Connection capability atom (
connection/IConnectionCapabilities.ts) — the same split as the ADT atoms above, for the same reason:IAbapConnectionis the minimum every transport can honour, and this is a thing only some can.ISessionLifecycleAware—disconnect(),flushGoodbye(),isConnected(),getSessionIdentity()disconnect()takes no arguments (since 18.0.0). It notifies: it tells the server the session is finished, and whether and when the session is actually freed is the server's affair — nothing checks afterwards. That is why there is no deadline to pass; waiting for the answer to a message nobody acts on buys a caller nothing, while being the one thing that could make a teardown unbounded, since a goodbye carries no request timeout by design- It resolves to
voidand always settles. Whatever it could not finish is the connection's own state, and a repeat call performs what is still owed — but a repeat call does not wait for the goodbye either, which is whatflushGoodbye()is for flushGoodbye(timeoutMs?)(since 39.0.0) — waits for the goodbyedisconnect()dispatched, and the caller who needs it is the one who reconnects:disconnect()thenconnect()opens the next session while the previous one's goodbye is still being assembled, and the server keeps both. Measured on E19 through@mcp-abap-adt/adt-clients, whose harness recycled the session after each test — a new ABAP session every one to two seconds for a whole run, none released, each living to its own thirty-minute idle timeout- The budget bounds the waiting, not the overlap. Finishes in time and there is none; does not, and the caller proceeds while the goodbye stays outstanding for as long as it takes. A return is not a confirmation and not even of dispatch — waiting out the budget resolves the same way as finishing, and at that point the request may not have reached the wire. It is on this atom rather than one of its own because waiting for the goodbye is not a separate capability from sending it
getSessionIdentity()names which server session the connection is on. A stable client-side conversation id says nothing about whether the server replaced the session underneath it — compare two readings across an operation to detect a replacementnullis not a verdict on the connection: it means no identity is known, which happens both when no session exists and when the connection is live over a server that issues no session cookie. UseisConnected()for connection state. It follows thatnull→ non-null is not a replacement, only a changed value is
ADT_SESSION_ERROR/AdtSessionErrorCode—ADT_NOT_CONNECTED,ADT_SESSION_REPLACED,ADT_RELEASE_PENDING. Match on the code, not on the messageAdditive to
IAbapConnection, which is unchanged. An RFC connection, a batch recorder and a test stub are all legitimate connections that own no HTTP session; making these methods mandatory would force each of them to implement a lie. A compile-time proof in__typechecks__/connectionCapabilities.tsasserts a session-less connection still satisfiesIAbapConnectionICriticalSection(since 38.1.0) —beginCriticalSection()/endCriticalSection(), nesting. Inside a section the connection's ordinary per-request deadline does not apply. It does not promise that no request can be cut short: an implementation may keep a far larger ceiling —@mcp-abap-adt/connectionraises it toSAP_TIMEOUT_CRITICAL, ten minutes by default — and a socket or the process ends a request whatever a contract says. What it is for: alock→ write →unlocksequence wants to run to completion, and aborting one of its requests part way ends nothing on the server — it ends what this side knows, so whether the write applied becomes unanswerable and the handleunlockneeds is lost while the lock lives on. Measured on a BTP trial: aPOST …/deletion/deleteabandoned at 45 s, then400 … Session Timed Out or Not Foundcarrying a new session cookie — over HTTP a session is two layers, the ICF one the cookie addresses and the ABAP one beneath it holding the enqueue locks, and the abort replaces the first while stranding the secondIRequestProfiling(since 38.1.0) —setProfilingRequest(what: string | null)/getProfilingRequest(), the connection-wideX-sap-adt-profilingdefault withnullfor none. A single request overrides it throughheaderson the request options, so the atom is for the default and not the one-off. The value is a string rather than an enum because what a server accepts there is the server's business;'server-time'is what Eclipse asks forIDeferredResponseConnection(since 14.0.0) — marks a connection (typically a batch recorder) whose responses resolve only after a later flush, so awaiting one mid-recording would deadlock. The atom carries no dependency onIAbapConnection, so a caller narrows whatever they already hold with a guard of their own:function hasDeferredResponses<T extends object>( connection: T, ): connection is T & IDeferredResponseConnection { return ( (connection as Partial<IDeferredResponseConnection>) .responsesAreDeferred === true ); }
IWebSocketTransport- Generic realtime transport contract for WS-based flows- Methods:
connect(),disconnect(),send(),onMessage(),onOpen(),onError(),onClose(),isConnected()
- Methods:
IWebSocketConnectOptions- WS connect options (protocols,headers, timeouts, heartbeat)IWebSocketMessageEnvelope- Generic request/response/event/error message shape with correlation idIWebSocketCloseInfo/IWebSocketMessageHandler- Close payload and message callback contractsIAbapRequestOptions- Request options for ADT operations
Feeds Domain (feeds/)
IAbapTimestamp- ABAP timestamp string type alias (formatYYYYMMDDHHMMSS)IFeedRepository- Domain-facing interface for feed access- Methods:
list(),variants(category),dumps(),systemMessages(),gatewayErrors(),gatewayErrorDetail() variantstakes a requiredcategorysince 26.0.0 — the endpoint answers400 "Parameter category could not be found."without one, so the parameterless call the previous signature allowed could not work- All methods return domain types (no raw transport responses)
- Methods:
IFeedQueryOptions- Query parameters for feed methods (user,maxResults,from,to)- The feed shapes left in 31.0.0 —
IFeedEntry,IFeedDescriptor,IFeedVariant,ISystemMessageEntry,IGatewayErrorEntry,IGatewayErrorDetail.IFeedRepositorytakes them as six type parameters, so what a feed reading answers is the implementation's. - The gateway-error internals (
IGatewayException,ICallStackEntry,ISourceCodeLine) left in 31.0.0 with the entry they were part of.
Execution Domain (execution/)
IAdtRunnable<TTarget, TResult, TOptions>(since 16.0.0)run(target, options?)— the whole of being executable. Everything else an executing handler offers is a different capability with its own interface.
IRunnableWithProfiler<TTarget, TResult, TOptions>andIRunnableWithProfiling<TTarget, TResult, TOptions>(since 30.0.0) — one method each: attaching a run to a profiler that is already recording, and asking for a measurement to be taken.IExecutoris gone: it bundled these two and inheritedrunon top, which made "runs a class" and "profiles a class" one thing an implementer had to take whole — and it was a second name for whatIAdtRunnablealready said, a target and some options answering something- Executors (
execution/IAdtExecutors.ts, since 14.0.0) —IClassExecutor/IProgramExecutor, each the intersection of those three atoms instantiated for its target (IClassExecutionTarget/IProgramExecutionTarget) with its profiler options and profiling result (IClassExecuteWithProfiler/ProfilingOptions,IClassExecuteWithProfilingResult, and the program equivalents). Since 22.0.0 neither result carries atraceId: the trace is written asynchronously, so at the moment a run returns there may be no trace, there may never be one, and the caller may read it a week later. Find it afterwards withIProfiler.list(). Moved verbatim fromadt-clients'AdtExecutor, which still owns the implementation. - Trace scheduling (
execution/ITraceScheduling.ts, since 22.0.0) —listObjectTypes(),listProcessTypes(),listRequests(),getRequestsByUri(),scheduleTrace(), composed into the two executors. There is deliberately no operation that submits a trace request: the stored entry is measured, the submitted document is not, and a published method would tell a consumer its argument is the wire shape on the strength of having read the response. Additive in a minor once a capture exists. Deliberately not onIAdtRunnableor the profiler atoms: ATC and unit-test runners implement those and have no business answering for trace parameters.
Runtime Domain (runtime/)
- No shared base.
IRuntimeAnalysisObjectandIListableRuntimeObjectwere deleted in 30.0.0: they existed only to be inherited, so a consumer wanting the listing had to take the discriminator and the other way round. Each runtime contract declares its ownreadonly kindand its ownlist()— a line each, and self-contained - Debugger and memory snapshots: not published here.
IDebugger,IAdtDebuggerSessionandIMemorySnapshotsleft in 30.0.0 for a research branch of@mcp-abap-adt/adt-clientsand come back measured: 39 ofIDebugger's 42 members answeredIAdtWireResponse, which is what a contract looks like before anyone knows what its endpoints return, and how memory snapshots are meant to function is still open. Batch is the precedent — a contract nobody can yet state should not be published, because every consumer that adopts it has to be migrated again when it changes. - Profiler:
IProfiler<TEntry, TViews>— a published composition ofITraceFamily,ITraceListing,ITraceReadingandITraceDeletion, taking its readings as parameters with no defaults, exactly asIClassExecutorandICrossTracedo. The composition is a contract: a consumer needs it to type a profiler and to implement one, and spelling that intersection by hand in every consumer is what publishing it prevents. What left in 31.0.0 are the shapes it used to name —IAbapTraceEntry,IAbapTraceViewsand the view results.list()gives what traces exist,read(traceId, view)what is inside one and answers that view's own type,delete(traceId)takes one back out. The request side stayed:IProfilerListOptions,IProfilerTraceParametersand the three per-view option types; everything about configuring a measurement isITraceScheduling. - Traces:
ICrossTrace(cross-layer traces),ISt05Trace(SQL trace) - Logs:
IApplicationLog,IAtcLog(ATC check logs) - DDIC:
IDdicActivation(activation graphs) - Dumps:
IRuntimeDumps(runtime dumps with views) - Feeds:
ISystemMessages,IGatewayErrorLog(reuseIFeedQueryOptions) - All runtime interfaces use literal
kinddiscriminators (e.g.,'profiler','debugger') for type-safe narrowing
SAP Domain (sap/)
ISapConfig- SAP connection configurationSapAuthType- Authentication type:"basic" | "jwt"
Service Domain (service/)
A service binding has no interface of its own, and that is deliberate. It is the capability atoms, composed — a consumer spells the half they need and TypeScript matches it structurally:
type PublishingOnly = IAdtUpdatable< Partial<IServiceBindingConfig> & Required< Pick< IServiceBindingConfig, 'bindingName' | 'desiredPublicationState' | 'serviceType' > >, void >;Publishing is an update:
desiredPublicationStateis a field of the config, not a method name. And since 37.0.0 the atom takes that config as given, so the three fields a publication cannot proceed without — which object, which state, and the protocol that selects the endpoint — are required at the call site instead of being flattened to optional on the way through.IAdtServiceBinding<R>andIServiceBindingResultswere removed — the last per-object aggregate in the package, and half of what it declared had no implementation left.Parameter/enum types:
ServiceBindingVariant—'ODATA_V2_UI' | 'ODATA_V2_WEB_API' | 'ODATA_V4_UI' | 'ODATA_V4_WEB_API'SERVICE_BINDING_VARIANT_MAP— maps variant to{ bindingType, bindingVersion, bindingCategory, serviceType }ServiceBindingType,ServiceBindingVersion,GeneratedServiceType,DesiredPublicationStateICreateServiceBindingParams(usesbinding_variant: ServiceBindingVariant),IUpdateServiceBindingParams,IReadServiceBindingParamsITransportCheckServiceBindingParams,ICheckServiceBindingParams,IActivateServiceBindingParamsIGenerateServiceBindingParams,ICreateAndGenerateServiceBindingParams
Storage Domain (storage/)
ISessionStorage- Session storage interfaceISessionState- Session state structure
Logging Domain (logging/)
ILogger- Logger interfaceLogLevel- Log level enum (ERROR = 0,WARN = 1,INFO = 2,DEBUG = 3)- Exported from package root:
import { LogLevel } from '@mcp-abap-adt/interfaces'
- Exported from package root:
Validation Domain (validation/)
IValidatedAuthConfig- Validated authentication configurationIHeaderValidationResult- Header validation resultAuthMethodPriority- Authentication method priority enum
Utilities Domain (utils/)
ITokenRefreshResult- Token refresh resultITimeoutConfig- Timeout configuration
Dependencies
The facade depends on the four @mcp-abap-adt/interfaces-* packages and re-exports them. Those depend on no implementation and no runtime package; interfaces-adt depends on interfaces-auth and interfaces-utils.
License
GNU Lesser General Public License v3.0 only (LGPL-3.0-only).
Earlier published versions were MIT and stay MIT — a licence change is not
retroactive.
Copyright © 2025–2026 Oleksii Kyslytsia
This library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3.
It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
Both texts ship with the package and both are needed: LICENSE is the
LGPL, COPYING is the GPL it is written on top of, since the LGPL is a
set of additional permissions over the GPL and cannot be read alone.
What this means if you depend on this package. Linking it into your own program — importing it, as every consumer of an npm package does — does not put your program under the LGPL. What the licence asks is that changes to this library stay free, and that your users can replace it with their own build.
