@objectstack/connector-rest 7.9.0 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +44 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/rest-connector.test.ts +21 -0
- package/src/rest-connector.ts +3 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/connector-rest@
|
|
2
|
+
> @objectstack/connector-rest@8.0.1 build /home/runner/work/framework/framework/packages/connectors/connector-rest
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[32m4.
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[32m12.
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[32m5.
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[32m13.
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m4.64 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m12.96 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 66ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.72 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m13.91 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 68ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 13016ms
|
|
21
21
|
[32mDTS[39m [1mdist/index.d.mts [22m[32m3.62 KB[39m
|
|
22
22
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m3.62 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @objectstack/connector-rest
|
|
2
2
|
|
|
3
|
+
## 8.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @objectstack/spec@8.0.1
|
|
8
|
+
- @objectstack/core@8.0.1
|
|
9
|
+
|
|
10
|
+
## 8.0.0
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- d5a8161: feat(spec): resilientFetch — timeout + backoff for outbound HTTP (P1-1)
|
|
15
|
+
|
|
16
|
+
Outbound calls in the connectors/embedder were naked `fetch` with no timeout or
|
|
17
|
+
retry, so a slow or rate-limited external API could hang an agent turn with no
|
|
18
|
+
recovery.
|
|
19
|
+
|
|
20
|
+
New shared `resilientFetch` (`@objectstack/spec/shared`):
|
|
21
|
+
|
|
22
|
+
- per-attempt timeout via `AbortController` (default 30s);
|
|
23
|
+
- exponential backoff with jitter, up to 3 attempts, on network errors / 429 / 5xx;
|
|
24
|
+
- honours a `Retry-After` header on 429;
|
|
25
|
+
- never retries a caller-initiated abort (intentional cancellation).
|
|
26
|
+
|
|
27
|
+
Wired into `connector-rest`, `connector-slack`, and `embedder-openai`.
|
|
28
|
+
`connector-mcp` talks through the MCP SDK transport, so it gets a 30s per-request
|
|
29
|
+
`timeout` on `callTool` / `listTools` instead.
|
|
30
|
+
|
|
31
|
+
A stateful per-host **circuit breaker** is deliberately left as a follow-up:
|
|
32
|
+
timeout + backoff already removes the hang/no-recovery risk.
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [a46c017]
|
|
35
|
+
- Updated dependencies [b990b89]
|
|
36
|
+
- Updated dependencies [99111ec]
|
|
37
|
+
- Updated dependencies [d5a8161]
|
|
38
|
+
- Updated dependencies [5cf1f1b]
|
|
39
|
+
- Updated dependencies [9ef89d4]
|
|
40
|
+
- Updated dependencies [3306d2f]
|
|
41
|
+
- Updated dependencies [c262301]
|
|
42
|
+
- Updated dependencies [bc44195]
|
|
43
|
+
- Updated dependencies [9e2e229]
|
|
44
|
+
- @objectstack/spec@8.0.0
|
|
45
|
+
- @objectstack/core@8.0.0
|
|
46
|
+
|
|
3
47
|
## 7.9.0
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
27
|
|
|
28
28
|
// src/rest-connector.ts
|
|
29
|
+
var import_shared = require("@objectstack/spec/shared");
|
|
29
30
|
function buildUrl(baseUrl, path, query) {
|
|
30
31
|
const base = baseUrl.replace(/\/+$/, "");
|
|
31
32
|
const suffix = path ? path.startsWith("/") ? path : `/${path}` : "";
|
|
@@ -58,7 +59,6 @@ function applyAuth(auth, headers, query) {
|
|
|
58
59
|
function createRestConnector(opts) {
|
|
59
60
|
const name = opts.name ?? "rest";
|
|
60
61
|
const auth = opts.auth ?? { type: "none" };
|
|
61
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
62
62
|
const def = {
|
|
63
63
|
name,
|
|
64
64
|
label: opts.label ?? "REST Connector",
|
|
@@ -109,11 +109,11 @@ function createRestConnector(opts) {
|
|
|
109
109
|
if (hasBody && headers["Content-Type"] === void 0 && headers["content-type"] === void 0) {
|
|
110
110
|
headers["Content-Type"] = "application/json";
|
|
111
111
|
}
|
|
112
|
-
const response = await
|
|
112
|
+
const response = await (0, import_shared.resilientFetch)(url, {
|
|
113
113
|
method,
|
|
114
114
|
headers,
|
|
115
115
|
body: hasBody ? JSON.stringify(req.body) : void 0
|
|
116
|
-
});
|
|
116
|
+
}, { fetchImpl: opts.fetchImpl });
|
|
117
117
|
const contentType = response.headers.get("content-type") ?? "";
|
|
118
118
|
const parsed = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
119
119
|
return { status: response.status, ok: response.ok, body: parsed };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-rest\n *\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). The baseline automation engine ships the `connector_action`\n * dispatch node + an empty connector registry; this plugin populates the\n * registry with a `rest` connector exposing a `request` action.\n *\n * Static auth only (`none` / `api-key` / `basic` / `bearer`); OAuth2 refresh,\n * credential vaulting, and multi-tenant lifecycle are the enterprise tier.\n */\n\nexport {\n createRestConnector,\n type RestConnectorOptions,\n type RestConnectorBundle,\n type RestRequestInput,\n type RestAuth,\n} from './rest-connector.js';\nexport {\n ConnectorRestPlugin,\n type ConnectorRestPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-rest-plugin.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n const doFetch = opts.fetchImpl ?? fetch;\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-rest\n *\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). The baseline automation engine ships the `connector_action`\n * dispatch node + an empty connector registry; this plugin populates the\n * registry with a `rest` connector exposing a `request` action.\n *\n * Static auth only (`none` / `api-key` / `basic` / `bearer`); OAuth2 refresh,\n * credential vaulting, and multi-tenant lifecycle are the enterprise tier.\n */\n\nexport {\n createRestConnector,\n type RestConnectorOptions,\n type RestConnectorBundle,\n type RestRequestInput,\n type RestAuth,\n} from './rest-connector.js';\nexport {\n ConnectorRestPlugin,\n type ConnectorRestPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-rest-plugin.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\nimport { resilientFetch } from '@objectstack/spec/shared';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await resilientFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n }, { fetchImpl: opts.fetchImpl });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,oBAA+B;AAuD/B,SAAS,SAAS,SAAiB,MAAc,OAA2C;AACxF,QAAM,OAAO,QAAQ,QAAQ,QAAQ,EAAE;AACvC,QAAM,SAAS,OAAQ,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI,KAAM;AACnE,QAAM,MAAM,IAAI,IAAI,OAAO,MAAM;AACjC,MAAI,OAAO;AACP,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC9C,UAAI,UAAU,UAAa,UAAU,KAAM,KAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IACtF;AAAA,EACJ;AACA,SAAO,IAAI,SAAS;AACxB;AAMA,SAAS,UACL,MACA,SACA,OACI;AACJ,UAAQ,KAAK,MAAM;AAAA,IACf,KAAK;AACD;AAAA,IACJ,KAAK;AACD,cAAQ,eAAe,IAAI,UAAU,KAAK,KAAK;AAC/C;AAAA,IACJ,KAAK,SAAS;AACV,YAAM,UAAU,OAAO,KAAK,GAAG,KAAK,QAAQ,IAAI,KAAK,QAAQ,EAAE,EAAE,SAAS,QAAQ;AAClF,cAAQ,eAAe,IAAI,SAAS,OAAO;AAC3C;AAAA,IACJ;AAAA,IACA,KAAK;AACD,UAAI,KAAK,UAAW,OAAM,KAAK,SAAS,IAAI,KAAK;AAAA,UAC5C,SAAQ,KAAK,cAAc,WAAW,IAAI,KAAK;AACpD;AAAA,EACR;AACJ;AAEO,SAAS,oBAAoB,MAAiD;AACjF,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,OAAiB,KAAK,QAAQ,EAAE,MAAM,OAAO;AAEnD,QAAM,MAAiB;AAAA,IACnB;AAAA,IACA,OAAO,KAAK,SAAS;AAAA,IACrB,MAAM;AAAA,IACN,aAAa;AAAA,IACb,MAAM;AAAA,IACN,gBAAgB;AAAA;AAAA;AAAA,IAGhB,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,qBAAqB;AAAA,IACrB,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACL;AAAA,QACI,KAAK;AAAA,QACL,OAAO;AAAA,QACP,aAAa;AAAA,QACb,aAAa;AAAA,UACT,MAAM;AAAA,UACN,YAAY;AAAA,YACR,QAAQ,EAAE,MAAM,UAAU,aAAa,4BAA4B;AAAA,YACnE,MAAM,EAAE,MAAM,UAAU,aAAa,gCAAgC;AAAA,YACrE,SAAS,EAAE,MAAM,UAAU,aAAa,sBAAsB;AAAA,YAC9D,OAAO,EAAE,MAAM,UAAU,aAAa,mBAAmB;AAAA,YACzD,MAAM,EAAE,aAAa,0CAA0C;AAAA,UACnE;AAAA,QACJ;AAAA,QACA,cAAc;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACR,QAAQ,EAAE,MAAM,SAAS;AAAA,YACzB,IAAI,EAAE,MAAM,UAAU;AAAA,YACtB,MAAM,CAAC;AAAA,UACX;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAEA,iBAAe,QAAQ,OAAkE;AACrF,UAAM,MAAM;AACZ,UAAM,UAAU,IAAI,UAAU,OAAO,YAAY;AACjD,UAAM,UAAkC,EAAE,GAAG,KAAK,gBAAgB,GAAG,IAAI,QAAQ;AACjF,UAAM,QAAsE,EAAE,GAAG,IAAI,MAAM;AAE3F,cAAU,MAAM,SAAS,KAAK;AAE9B,UAAM,MAAM,SAAS,KAAK,SAAS,IAAI,QAAQ,IAAI,KAAK;AAExD,UAAM,UAAU,IAAI,SAAS,UAAa,WAAW,SAAS,WAAW;AACzE,QAAI,WAAW,QAAQ,cAAc,MAAM,UAAa,QAAQ,cAAc,MAAM,QAAW;AAC3F,cAAQ,cAAc,IAAI;AAAA,IAC9B;AAEA,UAAM,WAAW,UAAM,8BAAe,KAAK;AAAA,MACvC;AAAA,MACA;AAAA,MACA,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,IAC/C,GAAG,EAAE,WAAW,KAAK,UAAU,CAAC;AAIhC,UAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,UAAM,SAAS,YAAY,SAAS,kBAAkB,IAChD,MAAM,SAAS,KAAK,IACpB,MAAM,SAAS,KAAK;AAE1B,WAAO,EAAE,QAAQ,SAAS,QAAQ,IAAI,SAAS,IAAI,MAAM,OAAO;AAAA,EACpE;AAEA,SAAO,EAAE,KAAK,UAAU,EAAE,QAAQ,EAAE;AACxC;;;AC5IO,IAAM,sBAAN,MAA4C;AAAA,EAW/C,YAAY,SAAqC;AAVjD,gBAAO;AACP,mBAAU;AACV,gBAAO;AAEP;AAAA,wBAAe,CAAC,oCAAoC;AAOhD,SAAK,UAAU;AAAA,EACnB;AAAA,EAEA,MAAM,KAAK,MAAoC;AAAA,EAG/C;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC3C,QAAI;AACJ,QAAI;AACA,mBAAa,IAAI,WAAqC,YAAY;AAAA,IACtE,QAAQ;AACJ,mBAAa;AAAA,IACjB;AAEA,QAAI,CAAC,cAAc,OAAO,WAAW,sBAAsB,YAAY;AACnE,UAAI,OAAO,KAAK,gFAA2E;AAC3F;AAAA,IACJ;AAEA,UAAM,EAAE,KAAK,SAAS,IAAI,oBAAoB,KAAK,OAAO;AAC1D,eAAW,kBAAkB,KAAK,QAAQ;AAC1C,SAAK,aAAa;AAClB,SAAK,gBAAgB,IAAI;AACzB,QAAI,OAAO,KAAK,wCAAwC,IAAI,IAAI,cAAc;AAAA,EAClF;AAAA,EAEA,MAAM,KAAK,MAAoC;AAC3C,QAAI,KAAK,cAAc,KAAK,eAAe;AACvC,UAAI;AAAE,aAAK,WAAW,oBAAoB,KAAK,aAAa;AAAA,MAAG,QAAQ;AAAA,MAAe;AAAA,IAC1F;AAAA,EACJ;AACJ;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/rest-connector.ts
|
|
2
|
+
import { resilientFetch } from "@objectstack/spec/shared";
|
|
2
3
|
function buildUrl(baseUrl, path, query) {
|
|
3
4
|
const base = baseUrl.replace(/\/+$/, "");
|
|
4
5
|
const suffix = path ? path.startsWith("/") ? path : `/${path}` : "";
|
|
@@ -31,7 +32,6 @@ function applyAuth(auth, headers, query) {
|
|
|
31
32
|
function createRestConnector(opts) {
|
|
32
33
|
const name = opts.name ?? "rest";
|
|
33
34
|
const auth = opts.auth ?? { type: "none" };
|
|
34
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
35
35
|
const def = {
|
|
36
36
|
name,
|
|
37
37
|
label: opts.label ?? "REST Connector",
|
|
@@ -82,11 +82,11 @@ function createRestConnector(opts) {
|
|
|
82
82
|
if (hasBody && headers["Content-Type"] === void 0 && headers["content-type"] === void 0) {
|
|
83
83
|
headers["Content-Type"] = "application/json";
|
|
84
84
|
}
|
|
85
|
-
const response = await
|
|
85
|
+
const response = await resilientFetch(url, {
|
|
86
86
|
method,
|
|
87
87
|
headers,
|
|
88
88
|
body: hasBody ? JSON.stringify(req.body) : void 0
|
|
89
|
-
});
|
|
89
|
+
}, { fetchImpl: opts.fetchImpl });
|
|
90
90
|
const contentType = response.headers.get("content-type") ?? "";
|
|
91
91
|
const parsed = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
92
92
|
return { status: response.status, ok: response.ok, body: parsed };
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n const doFetch = opts.fetchImpl ?? fetch;\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\nimport { resilientFetch } from '@objectstack/spec/shared';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await resilientFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n }, { fetchImpl: opts.fetchImpl });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/connector-rest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "
|
|
17
|
-
"@objectstack/spec": "
|
|
16
|
+
"@objectstack/core": "8.0.1",
|
|
17
|
+
"@objectstack/spec": "8.0.1"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^25.9.1",
|
|
21
21
|
"typescript": "^6.0.3",
|
|
22
22
|
"vitest": "^4.1.8",
|
|
23
|
-
"@objectstack/service-automation": "
|
|
23
|
+
"@objectstack/service-automation": "8.0.1"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"objectstack",
|
|
@@ -48,6 +48,27 @@ describe('createRestConnector — request action', () => {
|
|
|
48
48
|
expect(out).toEqual({ status: 200, ok: true, body: { id: 1, name: 'Ada' } });
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
it('retries a transient 503 then returns the success (P1-1)', async () => {
|
|
52
|
+
let n = 0;
|
|
53
|
+
const calls: number[] = [];
|
|
54
|
+
const impl = (async () => {
|
|
55
|
+
calls.push(1);
|
|
56
|
+
const status = n++ === 0 ? 503 : 200;
|
|
57
|
+
return {
|
|
58
|
+
status,
|
|
59
|
+
ok: status >= 200 && status < 300,
|
|
60
|
+
headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
|
|
61
|
+
json: async () => ({ ok: status === 200 }),
|
|
62
|
+
text: async () => '',
|
|
63
|
+
};
|
|
64
|
+
}) as unknown as typeof fetch;
|
|
65
|
+
const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
|
|
66
|
+
|
|
67
|
+
const out = await handlers.request({ path: '/x' }, {});
|
|
68
|
+
expect(calls.length).toBe(2); // retried the 503 once
|
|
69
|
+
expect(out.status).toBe(200);
|
|
70
|
+
});
|
|
71
|
+
|
|
51
72
|
it('JSON-encodes the body and sets Content-Type for non-GET', async () => {
|
|
52
73
|
const { impl, calls } = stubFetch();
|
|
53
74
|
const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
|
package/src/rest-connector.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
import type { Connector } from '@objectstack/spec/integration';
|
|
4
|
+
import { resilientFetch } from '@objectstack/spec/shared';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Generic REST connector — the reference *concrete* connector (ADR-0018
|
|
@@ -97,7 +98,6 @@ function applyAuth(
|
|
|
97
98
|
export function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {
|
|
98
99
|
const name = opts.name ?? 'rest';
|
|
99
100
|
const auth: RestAuth = opts.auth ?? { type: 'none' };
|
|
100
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
101
101
|
|
|
102
102
|
const def: Connector = {
|
|
103
103
|
name,
|
|
@@ -154,11 +154,11 @@ export function createRestConnector(opts: RestConnectorOptions): RestConnectorBu
|
|
|
154
154
|
headers['Content-Type'] = 'application/json';
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
const response = await
|
|
157
|
+
const response = await resilientFetch(url, {
|
|
158
158
|
method,
|
|
159
159
|
headers,
|
|
160
160
|
body: hasBody ? JSON.stringify(req.body) : undefined,
|
|
161
|
-
});
|
|
161
|
+
}, { fetchImpl: opts.fetchImpl });
|
|
162
162
|
|
|
163
163
|
// Parse JSON when advertised; fall back to text so non-JSON endpoints
|
|
164
164
|
// don't throw.
|