@objectstack/connector-rest 17.0.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 CHANGED
@@ -1,5 +1,174 @@
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
+
73
+ ## 17.1.0
74
+
75
+ ### Patch Changes
76
+
77
+ - Updated dependencies [56656aa]
78
+ - Updated dependencies [07e630e]
79
+ - Updated dependencies [2f65b1b]
80
+ - Updated dependencies [720ee95]
81
+ - Updated dependencies [f287435]
82
+ - Updated dependencies [2782805]
83
+ - Updated dependencies [e43d63a]
84
+ - Updated dependencies [9aa8890]
85
+ - Updated dependencies [7c9c1dd]
86
+ - Updated dependencies [75b7c24]
87
+ - Updated dependencies [d5552ca]
88
+ - Updated dependencies [d9813a9]
89
+ - Updated dependencies [8640fb2]
90
+ - Updated dependencies [2420641]
91
+ - Updated dependencies [2ad91c3]
92
+ - Updated dependencies [f57fb38]
93
+ - Updated dependencies [00777a0]
94
+ - Updated dependencies [d491625]
95
+ - Updated dependencies [420804d]
96
+ - Updated dependencies [716ac9b]
97
+ - Updated dependencies [a38408a]
98
+ - Updated dependencies [62b1427]
99
+ - Updated dependencies [7ea1372]
100
+ - Updated dependencies [23abe27]
101
+ - Updated dependencies [985a9cd]
102
+ - Updated dependencies [5f5e234]
103
+ - Updated dependencies [a8189ae]
104
+ - Updated dependencies [26e70fb]
105
+ - Updated dependencies [42b05af]
106
+ - Updated dependencies [2b292ce]
107
+ - Updated dependencies [abcf853]
108
+ - Updated dependencies [8b9eba5]
109
+ - Updated dependencies [d575779]
110
+ - Updated dependencies [94f7ef8]
111
+ - Updated dependencies [c5ac5e4]
112
+ - Updated dependencies [a777944]
113
+ - Updated dependencies [dd88e1c]
114
+ - Updated dependencies [856527c]
115
+ - Updated dependencies [870f710]
116
+ - Updated dependencies [79c46da]
117
+ - Updated dependencies [7ff3975]
118
+ - Updated dependencies [29d055b]
119
+ - Updated dependencies [65589d6]
120
+ - Updated dependencies [2c86fe3]
121
+ - Updated dependencies [e196c6a]
122
+ - Updated dependencies [24173e9]
123
+ - Updated dependencies [4ab7523]
124
+ - Updated dependencies [19539b4]
125
+ - Updated dependencies [f8eb736]
126
+ - Updated dependencies [11b779e]
127
+ - Updated dependencies [739fe5b]
128
+ - Updated dependencies [4bfe1a5]
129
+ - Updated dependencies [2065e31]
130
+ - Updated dependencies [b69d0f5]
131
+ - Updated dependencies [4d47afe]
132
+ - Updated dependencies [e4e5c6e]
133
+ - Updated dependencies [9a56784]
134
+ - Updated dependencies [d00d2f6]
135
+ - Updated dependencies [df0c12d]
136
+ - Updated dependencies [d31785f]
137
+ - Updated dependencies [c308a4f]
138
+ - Updated dependencies [e2899f6]
139
+ - Updated dependencies [3851f87]
140
+ - Updated dependencies [2a29caa]
141
+ - Updated dependencies [09a6eee]
142
+ - Updated dependencies [1a7f907]
143
+ - Updated dependencies [cd455c8]
144
+ - Updated dependencies [e1bb0ca]
145
+ - Updated dependencies [30d3752]
146
+ - Updated dependencies [c80e7ae]
147
+ - Updated dependencies [09a9a8a]
148
+ - Updated dependencies [07026cf]
149
+ - Updated dependencies [5d4f3d5]
150
+ - Updated dependencies [4d80e8b]
151
+ - Updated dependencies [30b1c63]
152
+ - Updated dependencies [079b457]
153
+ - Updated dependencies [e43b211]
154
+ - Updated dependencies [890b38f]
155
+ - Updated dependencies [8bee54b]
156
+ - Updated dependencies [7a537ce]
157
+ - Updated dependencies [593c4bf]
158
+ - Updated dependencies [ff08691]
159
+ - Updated dependencies [60e0f90]
160
+ - Updated dependencies [90c5285]
161
+ - Updated dependencies [402c125]
162
+ - Updated dependencies [7901b2d]
163
+ - Updated dependencies [56bca91]
164
+ - Updated dependencies [79394d7]
165
+ - Updated dependencies [730fd9a]
166
+ - Updated dependencies [44bc51d]
167
+ - Updated dependencies [73cfddf]
168
+ - Updated dependencies [d634e66]
169
+ - @objectstack/spec@17.1.0
170
+ - @objectstack/core@17.1.0
171
+
3
172
  ## 17.0.0
4
173
 
5
174
  ### 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
- stop(_ctx: PluginContext): Promise<void>;
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
- stop(_ctx: PluginContext): Promise<void>;
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
- async stop(_ctx) {
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":"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","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
- async stop(_ctx) {
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 {
@@ -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.0.0",
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.0.0",
17
- "@objectstack/spec": "17.0.0"
16
+ "@objectstack/core": "17.2.0",
17
+ "@objectstack/spec": "17.2.0"
18
18
  },
19
19
  "devDependencies": {
20
- "@types/node": "^26.1.2",
20
+ "@types/node": "^26.2.0",
21
21
  "typescript": "^6.0.3",
22
22
  "vitest": "^4.1.10",
23
- "@objectstack/service-automation": "17.0.0"
23
+ "@objectstack/service-automation": "17.2.0"
24
24
  },
25
25
  "keywords": [
26
26
  "objectstack",