Skip to main content

Handler API

Handlers are the TypeScript functions an HTTP connector supplies, and the overrides an engine-based connector can add. Each handler is a module whose default export takes its input first and a context object, ctx, last. It returns a plain value and throws on failure. A connector may import only its own files (by relative path, or through the connector-root alias) and @owlie/connectors/sdk/v2.

Handler keys and files​

A handler's key names the operation. Its file is the key with / in place of ., except Grant handlers, which live under handlers/grants/.

KeyFileSignatureReturns
connection.validateConfighandlers/connection/validateConfig.ts(input, ctx){ valid: true } or { valid: false, errors }
connection.testConnectionhandlers/connection/testConnection.ts(input, ctx)Any value on success; throws on failure
connection.discoverhandlers/connection/discover.ts(input, ctx)LDAP only: what the directory offers. Read-only.
connection.validateStatementshandlers/connection/validateStatements.ts(input, ctx)SQL only: whether each statement plans. Read-only.
<entity>.fetchOnehandlers/<entity>/fetchOne.ts(identifiers, ctx){ record, metadata? }, or { record: null }
<entity>.fetchAllhandlers/<entity>/fetchAll.ts(options, ctx)A page
<entity>.fetchAllSincehandlers/<entity>/fetchAllSince.ts(options, ctx)A page; options.marker holds the saved marker
<entity>.fetchRelationshipshandlers/<entity>/fetchRelationships.ts(entityExternalId, options, ctx)A page of relationships
<entity>.createhandlers/<entity>/create.ts(input, ctx)The created record
<entity>.updatehandlers/<entity>/update.ts(identifiers, input, ctx)The updated record
<entity>.deletehandlers/<entity>/delete.ts(identifiers, ctx)Nothing
<entity>.enablehandlers/<entity>/enable.ts(identifiers, ctx)Nothing
<entity>.disablehandlers/<entity>/disable.ts(identifiers, ctx)Nothing
<entity>.op.<name>handlers/<entity>/op/<name>.ts(params, ctx)Any JSON value
grant.<kind>.addhandlers/grants/<kind>/add.ts(input, ctx)Nothing, a record, or ctx.grant.alreadyPresent()
grant.<kind>.removehandlers/grants/<kind>/remove.ts(input, ctx)Nothing, a record, or ctx.grant.alreadyAbsent()
notification.sendhandlers/notification/send.ts(input, ctx)Any value

A custom operation (<entity>.op.<name>) belongs on the account entity only; a handler on any other entity can never run.

Entity keys, operation names and Grant kind keys start with a letter and contain only letters, digits, _ and -. The entity keys grant, grants, connection and notification are reserved.

Importing your own files​

A handler reaches its own files with either of two forms, which can be mixed in one file:

ImportResolves to
@connector/lib/<path>lib/<path> in the connector, with the extension as written: @connector/lib/google.ts.
@connector/typesThe generated types.gen.ts at the connector's root.
A relative pathThe file at that path, such as ../../lib/lookups.ts. The depth depends on where the importing file sits.

The @connector/ forms do not change with the importing file's depth, so a file can move without its imports being rewritten. Nothing else under @connector/ exists: @connector/other fails the build with a message that names the two forms above, as does a lib/ file that is not in the connector. The build and the Connector Builder's editor resolve the alias the same way.

Types​

Owlie generates types.gen.ts from your manifest. Import from it as @connector/types, which reads the same from every folder. The relative path works too: ../../types.gen.ts from handlers/<entity>/, ../../../types.gen.ts from handlers/grants/<kind>/ and handlers/<entity>/op/, and ../types.gen.ts from lib/.

TypeWhat it is
Handler<"key">The exact function type for one handler key. A key your manifest does not route to does not exist.
ConfigThe install's configuration, { config, auth }: the shared settings under config, and the chosen authentication option's { key, config } under auth.
<Entity>Record, <Entity>IdentifiersOne record type and one identifiers type per entity, such as UserRecord and UserIdentifiers. Every identifier is optional, since callers pass whichever they have.
Grant input typesOne per writable Grant kind, with its qualifiers typed.
ContextThe type of ctx, for helpers in lib/.
MapError, CheckResponse, ClassifyResponse, AuthHooksThe types of the optional lib/ hooks.
Override<"key">Engine-based kinds only: the type of an override for one key, which can call ctx.engineDefault.

Handler inputs​

InputShape
input (connection.validateConfig)The configuration being checked, shaped { config, auth } like ctx.config. connection.testConnection receives an empty object; read the configuration from ctx.config.
identifiersAn object with any of the entity's identifier properties, such as { "id": "u_1042" } or { "email": "alex@example.com" }.
options (list handlers)pageSize, cursor (the cursor your previous page returned), filter, and for fetchAllSince, marker.
Grant inputgrantKind; principal: { entityKey, identifiers }; target: { identifiers }; qualifiers, an object of the Grant kind's qualifier values.
params (custom operations)The operation's parameters, already checked against its params_schema. ctx.identifiers holds the record it was called on.

A page is { records, nextCursor, hasMore }, with an optional marker (incremental entities) and a relationships array for embedded Grant kinds (empty when the page carries none). A relationship is { edgeKey, sourceEntityKey, sourceExternalId, targetEntityKey, targetExternalId, attrs? }.

The context​

MemberWhat it is
ctx.httpThe HTTP client. get, post, put, patch and delete take a path relative to the base URL and options (query, headers, json, body, timeoutMs, signal, parseAs). The response has status, headers and data. A non-2xx response throws a classified error.
ctx.paginatectx.paginate({ path, ... }) reads one page of a list call. See Pagination options.
ctx.optionsThe caller's signal and timeoutMs. Spread it into every ctx.http call.
ctx.configThe install's configuration, read-only, shaped { config, auth }: read a shared setting as ctx.config.config.subdomain, and the chosen option's key and settings as ctx.config.auth.key and ctx.config.auth.config. Never contains credentials Owlie manages for itself.
ctx.connectionMetadataValues captured when the integration was connected, if any.
ctx.entity{ key, name, isAccount } for an entity handler.
ctx.identifiersThe identifiers of the record a handler or custom operation was called on.
ctx.integration{ id, tenantId } of the running integration.
ctx.manifestThe manifest as Owlie runs it.
ctx.entitySchemasThe install's own entity schemas, when they differ from the manifest (engine-based kinds).
ctx.grantalreadyPresent() and alreadyAbsent(), for Grant handlers (Grant write contract).
ctx.errorsinvalidInput(message), notFound(resource, message), ConnectorSdkError, ConnectorApplicationError, rethrowKnownErrors(error).
ctx.validateschema(schema, data, options?) and resourceSchema(entityKey, data, options?). Use { mode: "warn" } for data the system returns.
ctx.logdebug, info, warn and error, each (message, context?). Context values under names that look secret (a token, a password) are redacted, but the message is not, so never put a secret in it. Owlie adds the tenant, integration and entity itself.
ctx.ldapLDAP on a private-network gateway: search, add, modify, delete, rename, readAll, and the dn and filter builders that escape values. Fails anywhere else. Signatures and result codes are in ctx.ldap.
ctx.sqlSQL on a private-network gateway: query, exec, transaction, validate. Values bind only as :name parameters. Fails anywhere else.
ctx.powershellrun(name, params, { timeoutMs }) runs the connector's scripts/<name>.ps1 on a private-network gateway and returns the JSON the script writes. Fails anywhere else.
ctx.engineDefaultOnly in an engine-based connector's override: calls the engine's own handler for the same key.

ctx.ldap​

ctx.ldap reaches the directory through the private-network gateway, which holds the connection and the bind credential; a handler never sees the directory's address or password. Every dn and base must be a whole DN. An attribute value is text, or bytes (a Uint8Array) for a binary attribute such as objectGUID.

MethodWhat it does
search({ base, scope, filter, attributes?, pageSize?, cookie? })Reads one page of entries. scope is base, one or sub; filter is a parenthesized filter; pageSize is required unless scope is base. Returns { entries, cookie }: each entry has dn and each requested attribute, as one value or a list. Pass cookie back to read the next page; it is null on the last page. A cookie belongs to the gateway's connection, so a separate test run cannot resume it.
add(dn, attributes)Creates an entry. attributes maps each attribute, objectClass included, to a value or a list.
modify(dn, changes)Changes an entry. Each change is { operation, attribute, values }, where operation is add, delete or replace. A replace with no values removes the attribute.
delete(dn)Deletes an entry.
rename(dn, newDn)Moves or renames an entry (LDAP modify DN).
readAll(dn, attribute)Every value of a multi-valued attribute as text, read in Active Directory's ranged windows where the directory uses them.

Build every DN and filter that carries a value with the builders, which escape it and refuse an invalid attribute name:

BuilderMethods
ctx.ldap.dnescape(value); child(parent, attribute, value), which returns attribute=value,parent with the value escaped and refuses a parent that is not a DN; isValid(dn).
ctx.ldap.filterescape(value); equals(attribute, value); startsWith(attribute, value); present(attribute); and(...filters), or(...filters) and not(filter), which refuse anything that is not a parenthesized filter; isValid(filter).

A call the directory refuses throws a connector error whose details.ldapResultCode is the LDAP result code and details.ldapResultName its name. applicationCodeOf(error) gives the mapped code for the common ones:

ResultldapResultCodeApplication code
No such attribute (value)16INVALID_VALUE
Constraint violation19INVALID_VALUE
Attribute or value exists20CONFLICT
No such object32NOT_FOUND
Unwilling to perform53PRECONDITION_FAILED
Entry already exists68ALREADY_EXISTS

ctx.ldap exists only on a gateway. Anywhere else every call fails with a configuration error, while the builders still work.

Pagination options​

OptionMeaning
pathThe list call, relative to the base URL. Required.
optionsPass the handler's options through, so cursor resumes and pageSize is honored.
stylecursor, next-token, offset-limit, page-number or link-header. Default: the manifest's pagination_type.
query, headersSent with every page.
itemsThe body key holding the records. Default: the body when it is an array, else the first array under items, data, results, records or value.
pageSize{ default, max }. Default page size 100.
paramsParameter names: offset, limit, page, pageSize, cursor.
firstPagepage-number only: the first page's number. Default 1.
totaloffset-limit and page-number: the body key holding the total. Default total.
fullPagesoffset-limit and page-number without a total: a short page is the last one.
nextcursor and next-token: where the next token is, { body: "key" } or { header: "Name" }.
operationThe caller's signal and timeout; filled from ctx.options when omitted.

The cursor ctx.paginate returns is an object ({ at, seen }); treat it as opaque. A walk that does not advance, a body without an item array, or a next URL on another origin is an error.

Limits​

In Owlie's cloud, each handler run (one page of a list, one Grant add, one test run) may use at most 10 seconds of CPU time, not counting time spent waiting on responses, and 1,000 outbound requests. A ctx.http request whose options set no timeoutMs times out after 20 seconds. Only HTTPS requests to the connector's allowed hosts leave Owlie's cloud; the manifest's egress and the install's egress_domains decide which hosts those are (Connector manifest).

Errors and predicates​

Exported from @owlie/connectors/sdk/v2:

ExportUse
isHttpStatus(error, status)The HTTP error had exactly this status.
isNotFound(error)A 404, or an error coded NOT_FOUND.
isAlreadyExists(error)An error coded ALREADY_EXISTS or ALREADY_MEMBER.
applicationCodeOf(error)The error's application code, wherever it is carried.
isHttpResponseError(error)The error came from a non-2xx response; its details carry appCode, providerCode and providerMessage.
isRateLimitedConnectorError(error)A 429, or a response classifyResponse flagged.
ConnectorSdkError, ConnectorApplicationErrorError classes to throw. ConnectorSdkError takes code, message, customerMessage and handledAt (use "connector.invocation-context").
pageItems(body, key?)The item array of a response body, for pages you read yourself. Returns an empty array when there is none, so check the body first.
isRecord(value)A plain object.
Type, StaticTypeBox, for record schemas you need in code.

ctx.http classifies a non-2xx response by status: 401 and 403 are AUTH_ERROR, retried once after a token refresh when the option can refresh; 404 is APP_ERROR with details.appCode NOT_FOUND; 400 and 422 are APP_ERROR with INVALID_VALUE; 412 is APP_ERROR with PRECONDITION_FAILED; 409 is APP_ERROR with no appCode; 429 is NETWORK_ERROR with QUOTA_EXCEEDED, retried after Retry-After; 5xx and network failures are NETWORK_ERROR, retried. details.providerCode and details.providerMessage carry the vendor's own error code and message.

lib/ hooks​

These files are optional. Owlie wires each one in when it exists.

FileExportRuns
lib/map-error.tsmapError(error, ctx)On every error ctx.http throws, before your handler sees it. Return a replacement or nothing.
lib/check-response.tscheckResponse(response, ctx)On every 2xx response, including every page. Throw to fail it; a plain Error becomes an APP_ERROR.
lib/classify-response.tsclassifyResponse({ status, headers, body })On every non-2xx response, before classification. Return { rateLimited: true, retryAfterMs? } or nothing.
lib/auth-hooks.tsauthHooksFor authentication stages marked { "custom": true }.

Authentication hooks​

authHooks is keyed by authentication option key. Each option supplies one function per stage it marks custom: acquire, credentials, refresh (with prepareRefresh, which builds the refresh request), apply, and optionally selectCredentialFamily. Each receives one object: auth ({ key }), config (the option's resolved configuration), credentials (the current ones), tokenResponse (after a token call), request (the request being authorized) and fetchImpl (a fetch that honors the connector's allowed hosts). A custom stage without its hook, or a hook no stage uses, is refused.

HookReturns
acquire, refresh{ credentials?, tokenResponse?, expiresAtMs? }. A plain object without those keys is taken as the credentials.
credentialsThe same shape; credentials becomes what apply reads as credentials.
prepareRefresh{ tokenUrl, body, headers? }: the refresh request, built without waiting on anything.
apply{ headers?, query?, basic?, baseUrl?, tls? }: headers and query parameters to add, basic as { username, password }, a base URL, tls as { cert, key }.
selectCredentialFamilyThe name of the credential family to use, for an option that holds more than one.

A connector you build cannot use a custom credentials stage together with browser consent (connect): Owlie refuses it when the admin completes the vendor's sign-in.