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/.
| Key | File | Signature | Returns |
|---|---|---|---|
connection.validateConfig | handlers/connection/validateConfig.ts | (input, ctx) | { valid: true } or { valid: false, errors } |
connection.testConnection | handlers/connection/testConnection.ts | (input, ctx) | Any value on success; throws on failure |
connection.discover | handlers/connection/discover.ts | (input, ctx) | LDAP only: what the directory offers. Read-only. |
connection.validateStatements | handlers/connection/validateStatements.ts | (input, ctx) | SQL only: whether each statement plans. Read-only. |
<entity>.fetchOne | handlers/<entity>/fetchOne.ts | (identifiers, ctx) | { record, metadata? }, or { record: null } |
<entity>.fetchAll | handlers/<entity>/fetchAll.ts | (options, ctx) | A page |
<entity>.fetchAllSince | handlers/<entity>/fetchAllSince.ts | (options, ctx) | A page; options.marker holds the saved marker |
<entity>.fetchRelationships | handlers/<entity>/fetchRelationships.ts | (entityExternalId, options, ctx) | A page of relationships |
<entity>.create | handlers/<entity>/create.ts | (input, ctx) | The created record |
<entity>.update | handlers/<entity>/update.ts | (identifiers, input, ctx) | The updated record |
<entity>.delete | handlers/<entity>/delete.ts | (identifiers, ctx) | Nothing |
<entity>.enable | handlers/<entity>/enable.ts | (identifiers, ctx) | Nothing |
<entity>.disable | handlers/<entity>/disable.ts | (identifiers, ctx) | Nothing |
<entity>.op.<name> | handlers/<entity>/op/<name>.ts | (params, ctx) | Any JSON value |
grant.<kind>.add | handlers/grants/<kind>/add.ts | (input, ctx) | Nothing, a record, or ctx.grant.alreadyPresent() |
grant.<kind>.remove | handlers/grants/<kind>/remove.ts | (input, ctx) | Nothing, a record, or ctx.grant.alreadyAbsent() |
notification.send | handlers/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:
| Import | Resolves to |
|---|---|
@connector/lib/<path> | lib/<path> in the connector, with the extension as written: @connector/lib/google.ts. |
@connector/types | The generated types.gen.ts at the connector's root. |
| A relative path | The 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/.
| Type | What it is |
|---|---|
Handler<"key"> | The exact function type for one handler key. A key your manifest does not route to does not exist. |
Config | The install's configuration, { config, auth }: the shared settings under config, and the chosen authentication option's { key, config } under auth. |
<Entity>Record, <Entity>Identifiers | One 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 types | One per writable Grant kind, with its qualifiers typed. |
Context | The type of ctx, for helpers in lib/. |
MapError, CheckResponse, ClassifyResponse, AuthHooks | The 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
| Input | Shape |
|---|---|
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. |
identifiers | An 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 input | grantKind; 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
| Member | What it is |
|---|---|
ctx.http | The 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.paginate | ctx.paginate({ path, ... }) reads one page of a list call. See Pagination options. |
ctx.options | The caller's signal and timeoutMs. Spread it into every ctx.http call. |
ctx.config | The 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.connectionMetadata | Values captured when the integration was connected, if any. |
ctx.entity | { key, name, isAccount } for an entity handler. |
ctx.identifiers | The identifiers of the record a handler or custom operation was called on. |
ctx.integration | { id, tenantId } of the running integration. |
ctx.manifest | The manifest as Owlie runs it. |
ctx.entitySchemas | The install's own entity schemas, when they differ from the manifest (engine-based kinds). |
ctx.grant | alreadyPresent() and alreadyAbsent(), for Grant handlers (Grant write contract). |
ctx.errors | invalidInput(message), notFound(resource, message), ConnectorSdkError, ConnectorApplicationError, rethrowKnownErrors(error). |
ctx.validate | schema(schema, data, options?) and resourceSchema(entityKey, data, options?). Use { mode: "warn" } for data the system returns. |
ctx.log | debug, 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.ldap | LDAP 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.sql | SQL on a private-network gateway: query, exec, transaction, validate. Values bind only as :name parameters. Fails anywhere else. |
ctx.powershell | run(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.engineDefault | Only 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.
| Method | What 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:
| Builder | Methods |
|---|---|
ctx.ldap.dn | escape(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.filter | escape(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:
| Result | ldapResultCode | Application code |
|---|---|---|
| No such attribute (value) | 16 | INVALID_VALUE |
| Constraint violation | 19 | INVALID_VALUE |
| Attribute or value exists | 20 | CONFLICT |
| No such object | 32 | NOT_FOUND |
| Unwilling to perform | 53 | PRECONDITION_FAILED |
| Entry already exists | 68 | ALREADY_EXISTS |
ctx.ldap exists only on a gateway. Anywhere else every call fails with a configuration error, while
the builders still work.
Pagination options
| Option | Meaning |
|---|---|
path | The list call, relative to the base URL. Required. |
options | Pass the handler's options through, so cursor resumes and pageSize is honored. |
style | cursor, next-token, offset-limit, page-number or link-header. Default: the manifest's pagination_type. |
query, headers | Sent with every page. |
items | The 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. |
params | Parameter names: offset, limit, page, pageSize, cursor. |
firstPage | page-number only: the first page's number. Default 1. |
total | offset-limit and page-number: the body key holding the total. Default total. |
fullPages | offset-limit and page-number without a total: a short page is the last one. |
next | cursor and next-token: where the next token is, { body: "key" } or { header: "Name" }. |
operation | The 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:
| Export | Use |
|---|---|
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, ConnectorApplicationError | Error 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, Static | TypeBox, 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.
| File | Export | Runs |
|---|---|---|
lib/map-error.ts | mapError(error, ctx) | On every error ctx.http throws, before your handler sees it. Return a replacement or nothing. |
lib/check-response.ts | checkResponse(response, ctx) | On every 2xx response, including every page. Throw to fail it; a plain Error becomes an APP_ERROR. |
lib/classify-response.ts | classifyResponse({ status, headers, body }) | On every non-2xx response, before classification. Return { rateLimited: true, retryAfterMs? } or nothing. |
lib/auth-hooks.ts | authHooks | For 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.
| Hook | Returns |
|---|---|
acquire, refresh | { credentials?, tokenResponse?, expiresAtMs? }. A plain object without those keys is taken as the credentials. |
credentials | The 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 }. |
selectCredentialFamily | The 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.