@objectstack/connector-rest 17.1.0 → 17.2.0
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/CHANGELOG.md +70 -0
- package/dist/index.d.mts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# @objectstack/connector-rest
|
|
2
2
|
|
|
3
|
+
## 17.2.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel
|
|
8
|
+
actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and
|
|
9
|
+
`destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and
|
|
10
|
+
`LiteKernel.destroy()`, which walk the plugins in reverse calling
|
|
11
|
+
`plugin.destroy()`, walked straight past every plugin whose teardown was spelled
|
|
12
|
+
`stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still
|
|
13
|
+
armed, the REST/OpenAPI/Slack connectors still registered on the automation
|
|
14
|
+
engine, the approvals SLA escalation job still scheduled, and the knowledge
|
|
15
|
+
event-sync subscription still open.
|
|
16
|
+
|
|
17
|
+
Each teardown body now lives in `destroy()`. `stop()` is retained as a
|
|
18
|
+
delegating alias with its parameter made optional, so an embedder that learned
|
|
19
|
+
to call it directly — precisely because the kernel never did — keeps working
|
|
20
|
+
unchanged. No export is removed and the `Plugin` interface is untouched.
|
|
21
|
+
|
|
22
|
+
Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as
|
|
23
|
+
fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted
|
|
24
|
+
from the merge queue.
|
|
25
|
+
- Updated dependencies [6936d07]
|
|
26
|
+
- Updated dependencies [59eb04d]
|
|
27
|
+
- Updated dependencies [9f05b7d]
|
|
28
|
+
- Updated dependencies [3b2af5e]
|
|
29
|
+
- Updated dependencies [7d2d112]
|
|
30
|
+
- Updated dependencies [5fa0d72]
|
|
31
|
+
- Updated dependencies [02b3b07]
|
|
32
|
+
- Updated dependencies [914c413]
|
|
33
|
+
- Updated dependencies [55809a0]
|
|
34
|
+
- Updated dependencies [ee2ff45]
|
|
35
|
+
- Updated dependencies [47cd3ec]
|
|
36
|
+
- Updated dependencies [52db1d1]
|
|
37
|
+
- Updated dependencies [5649efb]
|
|
38
|
+
- Updated dependencies [9d7d2de]
|
|
39
|
+
- Updated dependencies [c815c50]
|
|
40
|
+
- Updated dependencies [795ea05]
|
|
41
|
+
- Updated dependencies [2306a76]
|
|
42
|
+
- Updated dependencies [e5ea701]
|
|
43
|
+
- Updated dependencies [a40dcc1]
|
|
44
|
+
- Updated dependencies [def0d3e]
|
|
45
|
+
- Updated dependencies [8d0bb79]
|
|
46
|
+
- Updated dependencies [5acb58d]
|
|
47
|
+
- Updated dependencies [2e3cf95]
|
|
48
|
+
- Updated dependencies [4c93387]
|
|
49
|
+
- Updated dependencies [504c8d5]
|
|
50
|
+
- Updated dependencies [a037f7c]
|
|
51
|
+
- Updated dependencies [3ee8ddf]
|
|
52
|
+
- Updated dependencies [16cef97]
|
|
53
|
+
- Updated dependencies [a79bd35]
|
|
54
|
+
- Updated dependencies [6ceaa4b]
|
|
55
|
+
- Updated dependencies [15ea214]
|
|
56
|
+
- Updated dependencies [de19489]
|
|
57
|
+
- Updated dependencies [c684d00]
|
|
58
|
+
- Updated dependencies [923c424]
|
|
59
|
+
- Updated dependencies [1ec36b7]
|
|
60
|
+
- Updated dependencies [5f2e54c]
|
|
61
|
+
- Updated dependencies [189373b]
|
|
62
|
+
- Updated dependencies [35ad101]
|
|
63
|
+
- Updated dependencies [ceb33a9]
|
|
64
|
+
- Updated dependencies [73d9795]
|
|
65
|
+
- Updated dependencies [8012960]
|
|
66
|
+
- Updated dependencies [f34f56b]
|
|
67
|
+
- Updated dependencies [f399618]
|
|
68
|
+
- Updated dependencies [75e9301]
|
|
69
|
+
- Updated dependencies [2810695]
|
|
70
|
+
- @objectstack/spec@17.2.0
|
|
71
|
+
- @objectstack/core@17.2.0
|
|
72
|
+
|
|
3
73
|
## 17.1.0
|
|
4
74
|
|
|
5
75
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -90,7 +90,30 @@ declare class ConnectorRestPlugin implements Plugin {
|
|
|
90
90
|
constructor(options?: ConnectorRestPluginOptions);
|
|
91
91
|
init(ctx: PluginContext): Promise<void>;
|
|
92
92
|
start(ctx: PluginContext): Promise<void>;
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
|
|
95
|
+
* ONLY teardown entry point `ObjectKernel.performShutdown()` and
|
|
96
|
+
* `LiteKernel.destroy()` invoke.
|
|
97
|
+
*
|
|
98
|
+
* [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. `Plugin` declares
|
|
99
|
+
* `init()`, `start?()` and `destroy?()` and no `stop()`, so the kernel
|
|
100
|
+
* walked past this plugin at shutdown and the REST connector stayed registered in the automation
|
|
101
|
+
* engine for the lifetime of the process. `start()` IS on the
|
|
102
|
+
* interface, so the pair read as symmetric in review — that asymmetry is
|
|
103
|
+
* what let the same shape survive in six packages at once.
|
|
104
|
+
*
|
|
105
|
+
* No timers here, so this instance never cost a merge-queue eviction the
|
|
106
|
+
* way the `plugin-reports` / `service-messaging` members did (#9371). The
|
|
107
|
+
* class is the same one either way: a teardown the kernel does not reach.
|
|
108
|
+
*/
|
|
109
|
+
destroy(): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
112
|
+
* exported class, and removing it would break an embedder who learned to
|
|
113
|
+
* call it directly precisely BECAUSE the kernel never did. Prefer kernel
|
|
114
|
+
* shutdown; direct callers keep working unchanged.
|
|
115
|
+
*/
|
|
116
|
+
stop(_ctx?: PluginContext): Promise<void>;
|
|
94
117
|
private tryGetAutomation;
|
|
95
118
|
}
|
|
96
119
|
|
package/dist/index.d.ts
CHANGED
|
@@ -90,7 +90,30 @@ declare class ConnectorRestPlugin implements Plugin {
|
|
|
90
90
|
constructor(options?: ConnectorRestPluginOptions);
|
|
91
91
|
init(ctx: PluginContext): Promise<void>;
|
|
92
92
|
start(ctx: PluginContext): Promise<void>;
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
|
|
95
|
+
* ONLY teardown entry point `ObjectKernel.performShutdown()` and
|
|
96
|
+
* `LiteKernel.destroy()` invoke.
|
|
97
|
+
*
|
|
98
|
+
* [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. `Plugin` declares
|
|
99
|
+
* `init()`, `start?()` and `destroy?()` and no `stop()`, so the kernel
|
|
100
|
+
* walked past this plugin at shutdown and the REST connector stayed registered in the automation
|
|
101
|
+
* engine for the lifetime of the process. `start()` IS on the
|
|
102
|
+
* interface, so the pair read as symmetric in review — that asymmetry is
|
|
103
|
+
* what let the same shape survive in six packages at once.
|
|
104
|
+
*
|
|
105
|
+
* No timers here, so this instance never cost a merge-queue eviction the
|
|
106
|
+
* way the `plugin-reports` / `service-messaging` members did (#9371). The
|
|
107
|
+
* class is the same one either way: a teardown the kernel does not reach.
|
|
108
|
+
*/
|
|
109
|
+
destroy(): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
112
|
+
* exported class, and removing it would break an embedder who learned to
|
|
113
|
+
* call it directly precisely BECAUSE the kernel never did. Prefer kernel
|
|
114
|
+
* shutdown; direct callers keep working unchanged.
|
|
115
|
+
*/
|
|
116
|
+
stop(_ctx?: PluginContext): Promise<void>;
|
|
94
117
|
private tryGetAutomation;
|
|
95
118
|
}
|
|
96
119
|
|
package/dist/index.js
CHANGED
|
@@ -187,13 +187,40 @@ var ConnectorRestPlugin = class {
|
|
|
187
187
|
this.connectorName = def.name;
|
|
188
188
|
ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
/**
|
|
191
|
+
* The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
|
|
192
|
+
* ONLY teardown entry point `ObjectKernel.performShutdown()` and
|
|
193
|
+
* `LiteKernel.destroy()` invoke.
|
|
194
|
+
*
|
|
195
|
+
* [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. `Plugin` declares
|
|
196
|
+
* `init()`, `start?()` and `destroy?()` and no `stop()`, so the kernel
|
|
197
|
+
* walked past this plugin at shutdown and the REST connector stayed registered in the automation
|
|
198
|
+
* engine for the lifetime of the process. `start()` IS on the
|
|
199
|
+
* interface, so the pair read as symmetric in review — that asymmetry is
|
|
200
|
+
* what let the same shape survive in six packages at once.
|
|
201
|
+
*
|
|
202
|
+
* No timers here, so this instance never cost a merge-queue eviction the
|
|
203
|
+
* way the `plugin-reports` / `service-messaging` members did (#9371). The
|
|
204
|
+
* class is the same one either way: a teardown the kernel does not reach.
|
|
205
|
+
*/
|
|
206
|
+
async destroy() {
|
|
191
207
|
if (this.automation && this.connectorName) {
|
|
192
208
|
try {
|
|
193
209
|
this.automation.unregisterConnector(this.connectorName);
|
|
194
210
|
} catch {
|
|
195
211
|
}
|
|
196
212
|
}
|
|
213
|
+
this.automation = void 0;
|
|
214
|
+
this.connectorName = void 0;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
218
|
+
* exported class, and removing it would break an embedder who learned to
|
|
219
|
+
* call it directly precisely BECAUSE the kernel never did. Prefer kernel
|
|
220
|
+
* shutdown; direct callers keep working unchanged.
|
|
221
|
+
*/
|
|
222
|
+
async stop(_ctx) {
|
|
223
|
+
await this.destroy();
|
|
197
224
|
}
|
|
198
225
|
tryGetAutomation(ctx) {
|
|
199
226
|
try {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/rest-connector.ts","../src/rest-provider.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';\nexport {\n createRestProviderFactory,\n REST_PROVIDER_KEY,\n type RestProviderDeps,\n} from './rest-provider.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 { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestAuth } from './rest-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'rest'` is materialized by this factory.\n */\nexport const REST_PROVIDER_KEY = 'rest';\n\n/** Injectable dependencies for {@link createRestProviderFactory} (tests). */\nexport interface RestProviderDeps {\n /** Injected fetch implementation; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'rest'` declarative instance. */\ninterface RestProviderConfig {\n baseUrl?: unknown;\n defaultHeaders?: unknown;\n}\n\nfunction isStringRecord(v: unknown): v is Record<string, string> {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return false;\n return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');\n}\n\n/**\n * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the\n * automation service invokes it for each `provider: 'rest'` declarative instance,\n * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same\n * `{ def, handlers }` bundle {@link createRestConnector} produces for a\n * hand-wired REST connector — one `request` action over static-auth HTTP.\n *\n * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance\n * fails boot loudly rather than materializing a dead connector.\n */\nexport function createRestProviderFactory(deps: RestProviderDeps = {}): ConnectorProviderFactory {\n return (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as RestProviderConfig;\n if (typeof cfg.baseUrl !== 'string' || cfg.baseUrl.length === 0) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`,\n );\n }\n if (cfg.defaultHeaders !== undefined && !isStringRecord(cfg.defaultHeaders)) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string→string map.`,\n );\n }\n // `ResolvedConnectorAuth` is exactly the static-auth subset RestAuth expects\n // (the credentialRef has already been resolved to a secret upstream).\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createRestConnector({\n name: ctx.name,\n label: ctx.label,\n baseUrl: cfg.baseUrl,\n auth,\n defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : undefined,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\nimport { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so the plugin needs no runtime dependency on\n * `@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 registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no\n * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack\n * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`\n * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097\n * usage.\n */\nexport interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {}\n\n/**\n * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018\n * §Addendum) in two forms:\n *\n * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'rest'`\n * `connectors:` entries into live connectors at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `baseUrl`, it also registers one concrete `rest` connector at `start()`.\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/provider registries) exist 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 // Contribute the `rest` provider factory (ADR-0097). Done in init() — not\n // start() — so it is registered before the automation service materializes\n // declarative instances during its own start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n REST_PROVIDER_KEY,\n createRestProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorRestPlugin: registered 'rest' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Only register a concrete connector when a baseUrl was supplied — the\n // provider-only usage (no options) contributes just the factory in init().\n if (!this.options.baseUrl) return;\n\n const automation = this.tryGetAutomation(ctx);\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 as RestConnectorOptions);\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 private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;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;;;ACpKO,IAAM,oBAAoB;AAcjC,SAAS,eAAe,GAAyC;AAC/D,MAAI,CAAC,KAAK,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC5D,SAAO,OAAO,OAAO,CAA4B,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AACvF;AAYO,SAAS,0BAA0B,OAAyB,CAAC,GAA6B;AAC/F,SAAO,CAAC,QAAQ;AACd,UAAM,MAAO,IAAI,kBAAkB,CAAC;AACpC,QAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,WAAW,GAAG;AAC/D,YAAM,IAAI;AAAA,QACR,uCAAuC,IAAI,IAAI;AAAA,MACjD;AAAA,IACF;AACA,QAAI,IAAI,mBAAmB,UAAa,CAAC,eAAe,IAAI,cAAc,GAAG;AAC3E,YAAM,IAAI;AAAA,QACR,uCAAuC,IAAI,IAAI;AAAA,MACjD;AAAA,IACF;AAGA,UAAM,OAAO,IAAI;AACjB,WAAO,oBAAoB;AAAA,MACzB,MAAM,IAAI;AAAA,MACV,OAAO,IAAI;AAAA,MACX,SAAS,IAAI;AAAA,MACb;AAAA,MACA,gBAAgB,eAAe,IAAI,cAAc,IAAI,IAAI,iBAAiB;AAAA,MAC1E,WAAW,KAAK;AAAA,IAClB,CAAC;AAAA,EACH;AACF;;;AChBO,IAAM,sBAAN,MAA4C;AAAA,EAW/C,YAAY,UAAsC,CAAC,GAAG;AAVtD,gBAAO;AACP,mBAAU;AACV,gBAAO;AAEP;AAAA,wBAAe,CAAC,oCAAoC;AAOhD,SAAK,UAAU;AAAA,EACnB;AAAA,EAEA,MAAM,KAAK,KAAmC;AAI1C,UAAM,aAAa,KAAK,iBAAiB,GAAG;AAC5C,QAAI,cAAc,OAAO,WAAW,8BAA8B,YAAY;AAC1E,iBAAW;AAAA,QACP;AAAA,QACA,0BAA0B,EAAE,WAAW,KAAK,QAAQ,UAAU,CAAC;AAAA,MACnE;AACA,UAAI,OAAO,KAAK,2DAA2D;AAAA,IAC/E;AAAA,EACJ;AAAA,EAEA,MAAM,MAAM,KAAmC;AAG3C,QAAI,CAAC,KAAK,QAAQ,QAAS;AAE3B,UAAM,aAAa,KAAK,iBAAiB,GAAG;AAC5C,QAAI,CAAC,cAAc,OAAO,WAAW,sBAAsB,YAAY;AACnE,UAAI,OAAO,KAAK,gFAA2E;AAC3F;AAAA,IACJ;AAEA,UAAM,EAAE,KAAK,SAAS,IAAI,oBAAoB,KAAK,OAA+B;AAClF,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;AAAA,EAEQ,iBAAiB,KAA0D;AAC/E,QAAI;AACA,aAAO,IAAI,WAAqC,YAAY;AAAA,IAChE,QAAQ;AACJ,aAAO;AAAA,IACX;AAAA,EACJ;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/rest-connector.ts","../src/rest-provider.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';\nexport {\n createRestProviderFactory,\n REST_PROVIDER_KEY,\n type RestProviderDeps,\n} from './rest-provider.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 { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestAuth } from './rest-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'rest'` is materialized by this factory.\n */\nexport const REST_PROVIDER_KEY = 'rest';\n\n/** Injectable dependencies for {@link createRestProviderFactory} (tests). */\nexport interface RestProviderDeps {\n /** Injected fetch implementation; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'rest'` declarative instance. */\ninterface RestProviderConfig {\n baseUrl?: unknown;\n defaultHeaders?: unknown;\n}\n\nfunction isStringRecord(v: unknown): v is Record<string, string> {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return false;\n return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');\n}\n\n/**\n * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the\n * automation service invokes it for each `provider: 'rest'` declarative instance,\n * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same\n * `{ def, handlers }` bundle {@link createRestConnector} produces for a\n * hand-wired REST connector — one `request` action over static-auth HTTP.\n *\n * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance\n * fails boot loudly rather than materializing a dead connector.\n */\nexport function createRestProviderFactory(deps: RestProviderDeps = {}): ConnectorProviderFactory {\n return (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as RestProviderConfig;\n if (typeof cfg.baseUrl !== 'string' || cfg.baseUrl.length === 0) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`,\n );\n }\n if (cfg.defaultHeaders !== undefined && !isStringRecord(cfg.defaultHeaders)) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string→string map.`,\n );\n }\n // `ResolvedConnectorAuth` is exactly the static-auth subset RestAuth expects\n // (the credentialRef has already been resolved to a secret upstream).\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createRestConnector({\n name: ctx.name,\n label: ctx.label,\n baseUrl: cfg.baseUrl,\n auth,\n defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : undefined,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\nimport { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so the plugin needs no runtime dependency on\n * `@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 registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no\n * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack\n * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`\n * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097\n * usage.\n */\nexport interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {}\n\n/**\n * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018\n * §Addendum) in two forms:\n *\n * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'rest'`\n * `connectors:` entries into live connectors at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `baseUrl`, it also registers one concrete `rest` connector at `start()`.\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/provider registries) exist 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 // Contribute the `rest` provider factory (ADR-0097). Done in init() — not\n // start() — so it is registered before the automation service materializes\n // declarative instances during its own start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n REST_PROVIDER_KEY,\n createRestProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorRestPlugin: registered 'rest' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Only register a concrete connector when a baseUrl was supplied — the\n // provider-only usage (no options) contributes just the factory in init().\n if (!this.options.baseUrl) return;\n\n const automation = this.tryGetAutomation(ctx);\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 as RestConnectorOptions);\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 /**\n * The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the\n * ONLY teardown entry point `ObjectKernel.performShutdown()` and\n * `LiteKernel.destroy()` invoke.\n *\n * [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. `Plugin` declares\n * `init()`, `start?()` and `destroy?()` and no `stop()`, so the kernel\n * walked past this plugin at shutdown and the REST connector stayed registered in the automation\n * engine for the lifetime of the process. `start()` IS on the\n * interface, so the pair read as symmetric in review — that asymmetry is\n * what let the same shape survive in six packages at once.\n *\n * No timers here, so this instance never cost a merge-queue eviction the\n * way the `plugin-reports` / `service-messaging` members did (#9371). The\n * class is the same one either way: a teardown the kernel does not reach.\n */\n async destroy(): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n this.automation = undefined;\n this.connectorName = undefined;\n }\n\n /**\n * Retained alias for {@link destroy}. Kept because it is public API of an\n * exported class, and removing it would break an embedder who learned to\n * call it directly precisely BECAUSE the kernel never did. Prefer kernel\n * shutdown; direct callers keep working unchanged.\n */\n async stop(_ctx?: PluginContext): Promise<void> {\n await this.destroy();\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -158,13 +158,40 @@ var ConnectorRestPlugin = class {
|
|
|
158
158
|
this.connectorName = def.name;
|
|
159
159
|
ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
|
|
163
|
+
* ONLY teardown entry point `ObjectKernel.performShutdown()` and
|
|
164
|
+
* `LiteKernel.destroy()` invoke.
|
|
165
|
+
*
|
|
166
|
+
* [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. `Plugin` declares
|
|
167
|
+
* `init()`, `start?()` and `destroy?()` and no `stop()`, so the kernel
|
|
168
|
+
* walked past this plugin at shutdown and the REST connector stayed registered in the automation
|
|
169
|
+
* engine for the lifetime of the process. `start()` IS on the
|
|
170
|
+
* interface, so the pair read as symmetric in review — that asymmetry is
|
|
171
|
+
* what let the same shape survive in six packages at once.
|
|
172
|
+
*
|
|
173
|
+
* No timers here, so this instance never cost a merge-queue eviction the
|
|
174
|
+
* way the `plugin-reports` / `service-messaging` members did (#9371). The
|
|
175
|
+
* class is the same one either way: a teardown the kernel does not reach.
|
|
176
|
+
*/
|
|
177
|
+
async destroy() {
|
|
162
178
|
if (this.automation && this.connectorName) {
|
|
163
179
|
try {
|
|
164
180
|
this.automation.unregisterConnector(this.connectorName);
|
|
165
181
|
} catch {
|
|
166
182
|
}
|
|
167
183
|
}
|
|
184
|
+
this.automation = void 0;
|
|
185
|
+
this.connectorName = void 0;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
189
|
+
* exported class, and removing it would break an embedder who learned to
|
|
190
|
+
* call it directly precisely BECAUSE the kernel never did. Prefer kernel
|
|
191
|
+
* shutdown; direct callers keep working unchanged.
|
|
192
|
+
*/
|
|
193
|
+
async stop(_ctx) {
|
|
194
|
+
await this.destroy();
|
|
168
195
|
}
|
|
169
196
|
tryGetAutomation(ctx) {
|
|
170
197
|
try {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rest-connector.ts","../src/rest-provider.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 { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestAuth } from './rest-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'rest'` is materialized by this factory.\n */\nexport const REST_PROVIDER_KEY = 'rest';\n\n/** Injectable dependencies for {@link createRestProviderFactory} (tests). */\nexport interface RestProviderDeps {\n /** Injected fetch implementation; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'rest'` declarative instance. */\ninterface RestProviderConfig {\n baseUrl?: unknown;\n defaultHeaders?: unknown;\n}\n\nfunction isStringRecord(v: unknown): v is Record<string, string> {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return false;\n return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');\n}\n\n/**\n * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the\n * automation service invokes it for each `provider: 'rest'` declarative instance,\n * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same\n * `{ def, handlers }` bundle {@link createRestConnector} produces for a\n * hand-wired REST connector — one `request` action over static-auth HTTP.\n *\n * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance\n * fails boot loudly rather than materializing a dead connector.\n */\nexport function createRestProviderFactory(deps: RestProviderDeps = {}): ConnectorProviderFactory {\n return (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as RestProviderConfig;\n if (typeof cfg.baseUrl !== 'string' || cfg.baseUrl.length === 0) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`,\n );\n }\n if (cfg.defaultHeaders !== undefined && !isStringRecord(cfg.defaultHeaders)) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string→string map.`,\n );\n }\n // `ResolvedConnectorAuth` is exactly the static-auth subset RestAuth expects\n // (the credentialRef has already been resolved to a secret upstream).\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createRestConnector({\n name: ctx.name,\n label: ctx.label,\n baseUrl: cfg.baseUrl,\n auth,\n defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : undefined,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\nimport { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so the plugin needs no runtime dependency on\n * `@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 registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no\n * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack\n * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`\n * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097\n * usage.\n */\nexport interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {}\n\n/**\n * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018\n * §Addendum) in two forms:\n *\n * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'rest'`\n * `connectors:` entries into live connectors at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `baseUrl`, it also registers one concrete `rest` connector at `start()`.\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/provider registries) exist 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 // Contribute the `rest` provider factory (ADR-0097). Done in init() — not\n // start() — so it is registered before the automation service materializes\n // declarative instances during its own start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n REST_PROVIDER_KEY,\n createRestProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorRestPlugin: registered 'rest' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Only register a concrete connector when a baseUrl was supplied — the\n // provider-only usage (no options) contributes just the factory in init().\n if (!this.options.baseUrl) return;\n\n const automation = this.tryGetAutomation(ctx);\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 as RestConnectorOptions);\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 private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/rest-connector.ts","../src/rest-provider.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 { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestAuth } from './rest-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'rest'` is materialized by this factory.\n */\nexport const REST_PROVIDER_KEY = 'rest';\n\n/** Injectable dependencies for {@link createRestProviderFactory} (tests). */\nexport interface RestProviderDeps {\n /** Injected fetch implementation; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'rest'` declarative instance. */\ninterface RestProviderConfig {\n baseUrl?: unknown;\n defaultHeaders?: unknown;\n}\n\nfunction isStringRecord(v: unknown): v is Record<string, string> {\n if (!v || typeof v !== 'object' || Array.isArray(v)) return false;\n return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');\n}\n\n/**\n * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the\n * automation service invokes it for each `provider: 'rest'` declarative instance,\n * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same\n * `{ def, handlers }` bundle {@link createRestConnector} produces for a\n * hand-wired REST connector — one `request` action over static-auth HTTP.\n *\n * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance\n * fails boot loudly rather than materializing a dead connector.\n */\nexport function createRestProviderFactory(deps: RestProviderDeps = {}): ConnectorProviderFactory {\n return (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as RestProviderConfig;\n if (typeof cfg.baseUrl !== 'string' || cfg.baseUrl.length === 0) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`,\n );\n }\n if (cfg.defaultHeaders !== undefined && !isStringRecord(cfg.defaultHeaders)) {\n throw new Error(\n `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string→string map.`,\n );\n }\n // `ResolvedConnectorAuth` is exactly the static-auth subset RestAuth expects\n // (the credentialRef has already been resolved to a secret upstream).\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createRestConnector({\n name: ctx.name,\n label: ctx.label,\n baseUrl: cfg.baseUrl,\n auth,\n defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : undefined,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\nimport { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so the plugin needs no runtime dependency on\n * `@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 registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no\n * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack\n * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`\n * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097\n * usage.\n */\nexport interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {}\n\n/**\n * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018\n * §Addendum) in two forms:\n *\n * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'rest'`\n * `connectors:` entries into live connectors at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `baseUrl`, it also registers one concrete `rest` connector at `start()`.\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/provider registries) exist 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 // Contribute the `rest` provider factory (ADR-0097). Done in init() — not\n // start() — so it is registered before the automation service materializes\n // declarative instances during its own start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n REST_PROVIDER_KEY,\n createRestProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorRestPlugin: registered 'rest' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Only register a concrete connector when a baseUrl was supplied — the\n // provider-only usage (no options) contributes just the factory in init().\n if (!this.options.baseUrl) return;\n\n const automation = this.tryGetAutomation(ctx);\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 as RestConnectorOptions);\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 /**\n * The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the\n * ONLY teardown entry point `ObjectKernel.performShutdown()` and\n * `LiteKernel.destroy()` invoke.\n *\n * [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. `Plugin` declares\n * `init()`, `start?()` and `destroy?()` and no `stop()`, so the kernel\n * walked past this plugin at shutdown and the REST connector stayed registered in the automation\n * engine for the lifetime of the process. `start()` IS on the\n * interface, so the pair read as symmetric in review — that asymmetry is\n * what let the same shape survive in six packages at once.\n *\n * No timers here, so this instance never cost a merge-queue eviction the\n * way the `plugin-reports` / `service-messaging` members did (#9371). The\n * class is the same one either way: a teardown the kernel does not reach.\n */\n async destroy(): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n this.automation = undefined;\n this.connectorName = undefined;\n }\n\n /**\n * Retained alias for {@link destroy}. Kept because it is public API of an\n * exported class, and removing it would break an embedder who learned to\n * call it directly precisely BECAUSE the kernel never did. Prefer kernel\n * shutdown; direct callers keep working unchanged.\n */\n async stop(_ctx?: PluginContext): Promise<void> {\n await this.destroy();\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\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": "17.
|
|
3
|
+
"version": "17.2.0",
|
|
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.
|
|
17
|
-
"@objectstack/spec": "17.
|
|
16
|
+
"@objectstack/core": "17.2.0",
|
|
17
|
+
"@objectstack/spec": "17.2.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^26.2.0",
|
|
21
21
|
"typescript": "^6.0.3",
|
|
22
22
|
"vitest": "^4.1.10",
|
|
23
|
-
"@objectstack/service-automation": "17.
|
|
23
|
+
"@objectstack/service-automation": "17.2.0"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"objectstack",
|