@objectstack/connector-rest 15.0.0 → 15.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/connector-rest@15.0.0 build /home/runner/work/framework/framework/packages/connectors/connector-rest
2
+ > @objectstack/connector-rest@15.1.1 build /home/runner/work/framework/framework/packages/connectors/connector-rest
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- ESM dist/index.mjs 4.64 KB
14
- ESM dist/index.mjs.map 12.96 KB
15
- ESM ⚡️ Build success in 87ms
16
- CJS dist/index.js 5.72 KB
17
- CJS dist/index.js.map 13.91 KB
18
- CJS ⚡️ Build success in 93ms
13
+ ESM dist/index.mjs 6.23 KB
14
+ ESM dist/index.mjs.map 18.35 KB
15
+ ESM ⚡️ Build success in 75ms
16
+ CJS dist/index.js 7.42 KB
17
+ CJS dist/index.js.map 19.43 KB
18
+ CJS ⚡️ Build success in 77ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 15133ms
21
- DTS dist/index.d.mts 3.62 KB
22
- DTS dist/index.d.ts 3.62 KB
20
+ DTS ⚡️ Build success in 13889ms
21
+ DTS dist/index.d.mts 5.43 KB
22
+ DTS dist/index.d.ts 5.43 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,93 @@
1
1
  # @objectstack/connector-rest
2
2
 
3
+ ## 15.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - @objectstack/spec@15.1.1
8
+ - @objectstack/core@15.1.1
9
+
10
+ ## 15.1.0
11
+
12
+ ### Minor Changes
13
+
14
+ - f531a26: feat(connectors): ADR-0096 — provider-bound declarative connector instances materialized at boot (#2977)
15
+
16
+ Declarative `connectors:` stack entries used to be **descriptor-only** (#2612):
17
+ registered as metadata but never dispatchable, the platform's one dead metadata
18
+ surface. An entry may now name a **`provider`** — an installed generic executor
19
+ (`openapi` / `mcp` / `rest`) — and the automation service **materializes** it
20
+ into a live, dispatchable connector at boot. AI can now wire an integration as
21
+ pure metadata and a flow `connector_action` calls it end-to-end.
22
+
23
+ - **Schema (`@objectstack/spec`).** `ConnectorSchema` gains `provider`,
24
+ `providerConfig`, and `auth` (a `credentialRef`-based instance-auth shape —
25
+ `ConnectorInstanceAuthSchema` — that references credentials, never inlines
26
+ them); `authentication` now defaults to `{ type: 'none' }` so a provider-bound
27
+ instance need not author it (loosening — existing connectors are unaffected).
28
+ `DeclarativeConnectorEntrySchema` (used by `stack.zod.ts`) rejects inline
29
+ secrets, orphan `providerConfig`/`auth`, and authored `actions`/`triggers` on a
30
+ provider-bound entry. A new `integration/connector-provider.ts` defines the
31
+ provider-factory contract as pure types.
32
+
33
+ - **Engine + boot (`@objectstack/service-automation`).** The engine adds a
34
+ connector-provider registry (`registerConnectorProvider`/`getConnectorProvider`)
35
+ and origin-tags registered connectors. At boot the service resolves each
36
+ provider-bound entry — looking up the factory, resolving `auth.credentialRef`
37
+ via a pluggable `CredentialResolver` (open-tier default: environment
38
+ variables), and registering the materialized connector. Boot **fails loudly**
39
+ for an unknown provider, invalid `providerConfig`, an unresolvable
40
+ `credentialRef`, or a name conflict with a plugin-registered connector (no
41
+ silent precedence).
42
+
43
+ - **Providers (`connector-rest` / `connector-openapi` / `connector-mcp`).** Each
44
+ plugin registers a provider factory in `init()` reusing its existing
45
+ generator/adapter API. Plugin options are now **optional**: with none the
46
+ plugin contributes only its provider factory; with instance options it also
47
+ registers a hand-wired connector (back-compat). `connector-openapi` adds a
48
+ `ConnectorOpenApiPlugin`.
49
+
50
+ Open tier: static auth (`none`/`api-key`/`basic`/`bearer`) with `credentialRef`
51
+ resolved from env vars. Managed vaulting, OAuth2 refresh, and per-tenant
52
+ connection lifecycle remain the enterprise tier (ADR-0015) — an enterprise host
53
+ injects a vault-backed `CredentialResolver` with no change to the materialization
54
+ path.
55
+
56
+ ### Patch Changes
57
+
58
+ - Updated dependencies [f531a26]
59
+ - Updated dependencies [f531a26]
60
+ - Updated dependencies [f531a26]
61
+ - Updated dependencies [f531a26]
62
+ - Updated dependencies [f531a26]
63
+ - Updated dependencies [f531a26]
64
+ - Updated dependencies [3fe9df1]
65
+ - Updated dependencies [f531a26]
66
+ - Updated dependencies [f531a26]
67
+ - Updated dependencies [f531a26]
68
+ - Updated dependencies [f531a26]
69
+ - Updated dependencies [f531a26]
70
+ - Updated dependencies [f531a26]
71
+ - Updated dependencies [f531a26]
72
+ - Updated dependencies [f531a26]
73
+ - Updated dependencies [f531a26]
74
+ - Updated dependencies [f531a26]
75
+ - Updated dependencies [f531a26]
76
+ - Updated dependencies [f531a26]
77
+ - Updated dependencies [4109153]
78
+ - Updated dependencies [f531a26]
79
+ - Updated dependencies [f531a26]
80
+ - Updated dependencies [f531a26]
81
+ - Updated dependencies [f531a26]
82
+ - Updated dependencies [f531a26]
83
+ - Updated dependencies [f531a26]
84
+ - Updated dependencies [627f225]
85
+ - Updated dependencies [f531a26]
86
+ - Updated dependencies [f531a26]
87
+ - Updated dependencies [f531a26]
88
+ - @objectstack/spec@15.1.0
89
+ - @objectstack/core@15.1.0
90
+
3
91
  ## 15.0.0
4
92
 
5
93
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Connector } from '@objectstack/spec/integration';
1
+ import { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';
2
2
  import { Plugin, PluginContext } from '@objectstack/core';
3
3
 
4
4
  /**
@@ -48,20 +48,33 @@ declare function createRestConnector(opts: RestConnectorOptions): RestConnectorB
48
48
 
49
49
  /**
50
50
  * Minimal surface of the automation engine this plugin depends on — the
51
- * connector registry from ADR-0018 §Addendum. Kept structural so the plugin
52
- * needs no runtime dependency on `@objectstack/service-automation`.
51
+ * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).
52
+ * Kept structural so the plugin needs no runtime dependency on
53
+ * `@objectstack/service-automation`.
53
54
  */
54
55
  interface ConnectorRegistrySurface {
55
56
  registerConnector(def: Connector, handlers: Record<string, (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>>): void;
56
57
  unregisterConnector(name: string): void;
58
+ registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;
57
59
  }
58
- interface ConnectorRestPluginOptions extends RestConnectorOptions {
60
+ /**
61
+ * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no
62
+ * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack
63
+ * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`
64
+ * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097
65
+ * usage.
66
+ */
67
+ interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {
59
68
  }
60
69
  /**
61
- * ConnectorRestPlugin — registers a generic REST connector on the automation
62
- * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):
63
- * the dispatch node + registry are baseline; a connector like this one is a
64
- * plugin that populates the registry.
70
+ * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018
71
+ * §Addendum) in two forms:
72
+ *
73
+ * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the
74
+ * automation service can materialize declarative `provider: 'rest'`
75
+ * `connectors:` entries into live connectors at boot.
76
+ * 2. **Hand-wired instance** (optional, back-compat): when constructed with a
77
+ * `baseUrl`, it also registers one concrete `rest` connector at `start()`.
65
78
  *
66
79
  * If no automation engine is present the plugin logs and skips — the connector
67
80
  * has nowhere to register, which is not an error.
@@ -74,10 +87,33 @@ declare class ConnectorRestPlugin implements Plugin {
74
87
  private readonly options;
75
88
  private connectorName?;
76
89
  private automation?;
77
- constructor(options: ConnectorRestPluginOptions);
78
- init(_ctx: PluginContext): Promise<void>;
90
+ constructor(options?: ConnectorRestPluginOptions);
91
+ init(ctx: PluginContext): Promise<void>;
79
92
  start(ctx: PluginContext): Promise<void>;
80
93
  stop(_ctx: PluginContext): Promise<void>;
94
+ private tryGetAutomation;
81
95
  }
82
96
 
83
- export { type ConnectorRegistrySurface, ConnectorRestPlugin, type ConnectorRestPluginOptions, type RestAuth, type RestConnectorBundle, type RestConnectorOptions, type RestRequestInput, createRestConnector };
97
+ /**
98
+ * The provider key this package contributes (ADR-0097). A declarative
99
+ * `connectors:` entry with `provider: 'rest'` is materialized by this factory.
100
+ */
101
+ declare const REST_PROVIDER_KEY = "rest";
102
+ /** Injectable dependencies for {@link createRestProviderFactory} (tests). */
103
+ interface RestProviderDeps {
104
+ /** Injected fetch implementation; defaults to the global `fetch`. */
105
+ fetchImpl?: typeof fetch;
106
+ }
107
+ /**
108
+ * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the
109
+ * automation service invokes it for each `provider: 'rest'` declarative instance,
110
+ * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same
111
+ * `{ def, handlers }` bundle {@link createRestConnector} produces for a
112
+ * hand-wired REST connector — one `request` action over static-auth HTTP.
113
+ *
114
+ * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance
115
+ * fails boot loudly rather than materializing a dead connector.
116
+ */
117
+ declare function createRestProviderFactory(deps?: RestProviderDeps): ConnectorProviderFactory;
118
+
119
+ export { type ConnectorRegistrySurface, ConnectorRestPlugin, type ConnectorRestPluginOptions, REST_PROVIDER_KEY, type RestAuth, type RestConnectorBundle, type RestConnectorOptions, type RestProviderDeps, type RestRequestInput, createRestConnector, createRestProviderFactory };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Connector } from '@objectstack/spec/integration';
1
+ import { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';
2
2
  import { Plugin, PluginContext } from '@objectstack/core';
3
3
 
4
4
  /**
@@ -48,20 +48,33 @@ declare function createRestConnector(opts: RestConnectorOptions): RestConnectorB
48
48
 
49
49
  /**
50
50
  * Minimal surface of the automation engine this plugin depends on — the
51
- * connector registry from ADR-0018 §Addendum. Kept structural so the plugin
52
- * needs no runtime dependency on `@objectstack/service-automation`.
51
+ * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).
52
+ * Kept structural so the plugin needs no runtime dependency on
53
+ * `@objectstack/service-automation`.
53
54
  */
54
55
  interface ConnectorRegistrySurface {
55
56
  registerConnector(def: Connector, handlers: Record<string, (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>>): void;
56
57
  unregisterConnector(name: string): void;
58
+ registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;
57
59
  }
58
- interface ConnectorRestPluginOptions extends RestConnectorOptions {
60
+ /**
61
+ * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no
62
+ * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack
63
+ * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`
64
+ * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097
65
+ * usage.
66
+ */
67
+ interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {
59
68
  }
60
69
  /**
61
- * ConnectorRestPlugin — registers a generic REST connector on the automation
62
- * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):
63
- * the dispatch node + registry are baseline; a connector like this one is a
64
- * plugin that populates the registry.
70
+ * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018
71
+ * §Addendum) in two forms:
72
+ *
73
+ * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the
74
+ * automation service can materialize declarative `provider: 'rest'`
75
+ * `connectors:` entries into live connectors at boot.
76
+ * 2. **Hand-wired instance** (optional, back-compat): when constructed with a
77
+ * `baseUrl`, it also registers one concrete `rest` connector at `start()`.
65
78
  *
66
79
  * If no automation engine is present the plugin logs and skips — the connector
67
80
  * has nowhere to register, which is not an error.
@@ -74,10 +87,33 @@ declare class ConnectorRestPlugin implements Plugin {
74
87
  private readonly options;
75
88
  private connectorName?;
76
89
  private automation?;
77
- constructor(options: ConnectorRestPluginOptions);
78
- init(_ctx: PluginContext): Promise<void>;
90
+ constructor(options?: ConnectorRestPluginOptions);
91
+ init(ctx: PluginContext): Promise<void>;
79
92
  start(ctx: PluginContext): Promise<void>;
80
93
  stop(_ctx: PluginContext): Promise<void>;
94
+ private tryGetAutomation;
81
95
  }
82
96
 
83
- export { type ConnectorRegistrySurface, ConnectorRestPlugin, type ConnectorRestPluginOptions, type RestAuth, type RestConnectorBundle, type RestConnectorOptions, type RestRequestInput, createRestConnector };
97
+ /**
98
+ * The provider key this package contributes (ADR-0097). A declarative
99
+ * `connectors:` entry with `provider: 'rest'` is materialized by this factory.
100
+ */
101
+ declare const REST_PROVIDER_KEY = "rest";
102
+ /** Injectable dependencies for {@link createRestProviderFactory} (tests). */
103
+ interface RestProviderDeps {
104
+ /** Injected fetch implementation; defaults to the global `fetch`. */
105
+ fetchImpl?: typeof fetch;
106
+ }
107
+ /**
108
+ * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the
109
+ * automation service invokes it for each `provider: 'rest'` declarative instance,
110
+ * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same
111
+ * `{ def, handlers }` bundle {@link createRestConnector} produces for a
112
+ * hand-wired REST connector — one `request` action over static-auth HTTP.
113
+ *
114
+ * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance
115
+ * fails boot loudly rather than materializing a dead connector.
116
+ */
117
+ declare function createRestProviderFactory(deps?: RestProviderDeps): ConnectorProviderFactory;
118
+
119
+ export { type ConnectorRegistrySurface, ConnectorRestPlugin, type ConnectorRestPluginOptions, REST_PROVIDER_KEY, type RestAuth, type RestConnectorBundle, type RestConnectorOptions, type RestProviderDeps, type RestRequestInput, createRestConnector, createRestProviderFactory };
package/dist/index.js CHANGED
@@ -21,7 +21,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ConnectorRestPlugin: () => ConnectorRestPlugin,
24
- createRestConnector: () => createRestConnector
24
+ REST_PROVIDER_KEY: () => REST_PROVIDER_KEY,
25
+ createRestConnector: () => createRestConnector,
26
+ createRestProviderFactory: () => createRestProviderFactory
25
27
  });
26
28
  module.exports = __toCommonJS(index_exports);
27
29
 
@@ -121,25 +123,60 @@ function createRestConnector(opts) {
121
123
  return { def, handlers: { request } };
122
124
  }
123
125
 
126
+ // src/rest-provider.ts
127
+ var REST_PROVIDER_KEY = "rest";
128
+ function isStringRecord(v) {
129
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
130
+ return Object.values(v).every((x) => typeof x === "string");
131
+ }
132
+ function createRestProviderFactory(deps = {}) {
133
+ return (ctx) => {
134
+ const cfg = ctx.providerConfig ?? {};
135
+ if (typeof cfg.baseUrl !== "string" || cfg.baseUrl.length === 0) {
136
+ throw new Error(
137
+ `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`
138
+ );
139
+ }
140
+ if (cfg.defaultHeaders !== void 0 && !isStringRecord(cfg.defaultHeaders)) {
141
+ throw new Error(
142
+ `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string\u2192string map.`
143
+ );
144
+ }
145
+ const auth = ctx.auth;
146
+ return createRestConnector({
147
+ name: ctx.name,
148
+ label: ctx.label,
149
+ baseUrl: cfg.baseUrl,
150
+ auth,
151
+ defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : void 0,
152
+ fetchImpl: deps.fetchImpl
153
+ });
154
+ };
155
+ }
156
+
124
157
  // src/connector-rest-plugin.ts
125
158
  var ConnectorRestPlugin = class {
126
- constructor(options) {
159
+ constructor(options = {}) {
127
160
  this.name = "com.objectstack.connector.rest";
128
161
  this.version = "1.0.0";
129
162
  this.type = "standard";
130
- // Ensure the automation engine (and its connector registry) is started first.
163
+ // Ensure the automation engine (and its connector/provider registries) exist first.
131
164
  this.dependencies = ["com.objectstack.service-automation"];
132
165
  this.options = options;
133
166
  }
134
- async init(_ctx) {
167
+ async init(ctx) {
168
+ const automation = this.tryGetAutomation(ctx);
169
+ if (automation && typeof automation.registerConnectorProvider === "function") {
170
+ automation.registerConnectorProvider(
171
+ REST_PROVIDER_KEY,
172
+ createRestProviderFactory({ fetchImpl: this.options.fetchImpl })
173
+ );
174
+ ctx.logger.info("ConnectorRestPlugin: registered 'rest' connector provider");
175
+ }
135
176
  }
136
177
  async start(ctx) {
137
- let automation;
138
- try {
139
- automation = ctx.getService("automation");
140
- } catch {
141
- automation = void 0;
142
- }
178
+ if (!this.options.baseUrl) return;
179
+ const automation = this.tryGetAutomation(ctx);
143
180
  if (!automation || typeof automation.registerConnector !== "function") {
144
181
  ctx.logger.info("ConnectorRestPlugin: no automation engine \u2014 REST connector not registered");
145
182
  return;
@@ -158,10 +195,19 @@ var ConnectorRestPlugin = class {
158
195
  }
159
196
  }
160
197
  }
198
+ tryGetAutomation(ctx) {
199
+ try {
200
+ return ctx.getService("automation");
201
+ } catch {
202
+ return void 0;
203
+ }
204
+ }
161
205
  };
162
206
  // Annotate the CommonJS export names for ESM import in node:
163
207
  0 && (module.exports = {
164
208
  ConnectorRestPlugin,
165
- createRestConnector
209
+ REST_PROVIDER_KEY,
210
+ createRestConnector,
211
+ createRestProviderFactory
166
212
  });
167
213
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-rest\n *\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). The baseline automation engine ships the `connector_action`\n * dispatch node + an empty connector registry; this plugin populates the\n * registry with a `rest` connector exposing a `request` action.\n *\n * Static auth only (`none` / `api-key` / `basic` / `bearer`); OAuth2 refresh,\n * credential vaulting, and multi-tenant lifecycle are the enterprise tier.\n */\n\nexport {\n createRestConnector,\n type RestConnectorOptions,\n type RestConnectorBundle,\n type RestRequestInput,\n type RestAuth,\n} from './rest-connector.js';\nexport {\n ConnectorRestPlugin,\n type ConnectorRestPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-rest-plugin.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\nimport { resilientFetch } from '@objectstack/spec/shared';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await resilientFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n }, { fetchImpl: opts.fetchImpl });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
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":[]}
package/dist/index.mjs CHANGED
@@ -94,25 +94,60 @@ function createRestConnector(opts) {
94
94
  return { def, handlers: { request } };
95
95
  }
96
96
 
97
+ // src/rest-provider.ts
98
+ var REST_PROVIDER_KEY = "rest";
99
+ function isStringRecord(v) {
100
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
101
+ return Object.values(v).every((x) => typeof x === "string");
102
+ }
103
+ function createRestProviderFactory(deps = {}) {
104
+ return (ctx) => {
105
+ const cfg = ctx.providerConfig ?? {};
106
+ if (typeof cfg.baseUrl !== "string" || cfg.baseUrl.length === 0) {
107
+ throw new Error(
108
+ `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`
109
+ );
110
+ }
111
+ if (cfg.defaultHeaders !== void 0 && !isStringRecord(cfg.defaultHeaders)) {
112
+ throw new Error(
113
+ `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string\u2192string map.`
114
+ );
115
+ }
116
+ const auth = ctx.auth;
117
+ return createRestConnector({
118
+ name: ctx.name,
119
+ label: ctx.label,
120
+ baseUrl: cfg.baseUrl,
121
+ auth,
122
+ defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : void 0,
123
+ fetchImpl: deps.fetchImpl
124
+ });
125
+ };
126
+ }
127
+
97
128
  // src/connector-rest-plugin.ts
98
129
  var ConnectorRestPlugin = class {
99
- constructor(options) {
130
+ constructor(options = {}) {
100
131
  this.name = "com.objectstack.connector.rest";
101
132
  this.version = "1.0.0";
102
133
  this.type = "standard";
103
- // Ensure the automation engine (and its connector registry) is started first.
134
+ // Ensure the automation engine (and its connector/provider registries) exist first.
104
135
  this.dependencies = ["com.objectstack.service-automation"];
105
136
  this.options = options;
106
137
  }
107
- async init(_ctx) {
138
+ async init(ctx) {
139
+ const automation = this.tryGetAutomation(ctx);
140
+ if (automation && typeof automation.registerConnectorProvider === "function") {
141
+ automation.registerConnectorProvider(
142
+ REST_PROVIDER_KEY,
143
+ createRestProviderFactory({ fetchImpl: this.options.fetchImpl })
144
+ );
145
+ ctx.logger.info("ConnectorRestPlugin: registered 'rest' connector provider");
146
+ }
108
147
  }
109
148
  async start(ctx) {
110
- let automation;
111
- try {
112
- automation = ctx.getService("automation");
113
- } catch {
114
- automation = void 0;
115
- }
149
+ if (!this.options.baseUrl) return;
150
+ const automation = this.tryGetAutomation(ctx);
116
151
  if (!automation || typeof automation.registerConnector !== "function") {
117
152
  ctx.logger.info("ConnectorRestPlugin: no automation engine \u2014 REST connector not registered");
118
153
  return;
@@ -131,9 +166,18 @@ var ConnectorRestPlugin = class {
131
166
  }
132
167
  }
133
168
  }
169
+ tryGetAutomation(ctx) {
170
+ try {
171
+ return ctx.getService("automation");
172
+ } catch {
173
+ return void 0;
174
+ }
175
+ }
134
176
  };
135
177
  export {
136
178
  ConnectorRestPlugin,
137
- createRestConnector
179
+ REST_PROVIDER_KEY,
180
+ createRestConnector,
181
+ createRestProviderFactory
138
182
  };
139
183
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\nimport { resilientFetch } from '@objectstack/spec/shared';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await resilientFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n }, { fetchImpl: opts.fetchImpl });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/connector-rest",
3
- "version": "15.0.0",
3
+ "version": "15.1.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).",
6
6
  "main": "dist/index.js",
@@ -13,14 +13,14 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "15.0.0",
17
- "@objectstack/spec": "15.0.0"
16
+ "@objectstack/core": "15.1.1",
17
+ "@objectstack/spec": "15.1.1"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^26.1.1",
21
21
  "typescript": "^6.0.3",
22
22
  "vitest": "^4.1.10",
23
- "@objectstack/service-automation": "15.0.0"
23
+ "@objectstack/service-automation": "15.1.1"
24
24
  },
25
25
  "keywords": [
26
26
  "objectstack",
@@ -80,4 +80,75 @@ describe('ConnectorRestPlugin — end to end with the automation engine', () =>
80
80
 
81
81
  await kernel.shutdown();
82
82
  });
83
+
84
+ it('materializes a declarative `provider: rest` instance from stack metadata (ADR-0097)', async () => {
85
+ const { impl, calls } = stubFetch();
86
+
87
+ // A provider-bound `connectors:` entry as it sits in the ObjectQL registry.
88
+ const declared = [
89
+ {
90
+ name: 'billing',
91
+ label: 'Billing API',
92
+ type: 'api',
93
+ provider: 'rest',
94
+ providerConfig: { baseUrl: 'https://billing.example.com' },
95
+ auth: { type: 'bearer', credentialRef: 'BILLING_TOKEN' },
96
+ },
97
+ ];
98
+
99
+ const kernel = new LiteKernel();
100
+ // The plugin, with no baseUrl, contributes ONLY the `rest` provider factory.
101
+ kernel.use(new AutomationServicePlugin({ credentialResolver: (r) => (r === 'BILLING_TOKEN' ? 'sk-live' : undefined) }));
102
+ kernel.use(new ConnectorRestPlugin({ fetchImpl: impl }));
103
+ // A tiny harness that serves the declared connector metadata to the automation service.
104
+ kernel.use({
105
+ name: 'test.metadata',
106
+ type: 'standard',
107
+ version: '1.0.0',
108
+ dependencies: [],
109
+ async init(ctx: any) {
110
+ ctx.registerService('objectql', {
111
+ registry: { listItems: (t: string) => (t === 'connector' ? declared : []) },
112
+ });
113
+ },
114
+ async start() {},
115
+ } as never);
116
+
117
+ await kernel.bootstrap();
118
+ const engine = kernel.getService<AutomationEngine>('automation');
119
+
120
+ // Materialized under the declared name, tagged declarative, and listed.
121
+ expect(engine.getRegisteredConnectors()).toContain('billing');
122
+ expect(engine.getConnectorOrigin('billing')).toBe('declarative');
123
+ expect(engine.getConnectorDescriptors().find((d) => d.name === 'billing')?.actions.map((a) => a.key)).toEqual(['request']);
124
+
125
+ engine.registerFlow('call_billing', {
126
+ name: 'call_billing',
127
+ label: 'Call Billing',
128
+ type: 'autolaunched',
129
+ variables: [{ name: 'call.status', type: 'number', isOutput: true }],
130
+ nodes: [
131
+ { id: 'start', type: 'start', label: 'Start' },
132
+ {
133
+ id: 'call',
134
+ type: 'connector_action',
135
+ label: 'GET /invoices',
136
+ connectorConfig: { connectorId: 'billing', actionId: 'request', input: { path: '/invoices' } },
137
+ },
138
+ { id: 'end', type: 'end', label: 'End' },
139
+ ],
140
+ edges: [
141
+ { id: 'e1', source: 'start', target: 'call' },
142
+ { id: 'e2', source: 'call', target: 'end' },
143
+ ],
144
+ });
145
+
146
+ const result = await engine.execute('call_billing');
147
+ expect(result.success).toBe(true);
148
+ // Dispatched to the resolved base URL, with the credentialRef-resolved bearer token.
149
+ expect(calls[0].url).toBe('https://billing.example.com/invoices');
150
+ expect((calls[0].init.headers as Record<string, string>)['Authorization']).toBe('Bearer sk-live');
151
+
152
+ await kernel.shutdown();
153
+ });
83
154
  });
@@ -1,13 +1,15 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
3
  import type { Plugin, PluginContext } from '@objectstack/core';
4
- import type { Connector } from '@objectstack/spec/integration';
4
+ import type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';
5
5
  import { createRestConnector, type RestConnectorOptions } from './rest-connector.js';
6
+ import { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';
6
7
 
7
8
  /**
8
9
  * Minimal surface of the automation engine this plugin depends on — the
9
- * connector registry from ADR-0018 §Addendum. Kept structural so the plugin
10
- * needs no runtime dependency on `@objectstack/service-automation`.
10
+ * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).
11
+ * Kept structural so the plugin needs no runtime dependency on
12
+ * `@objectstack/service-automation`.
11
13
  */
12
14
  export interface ConnectorRegistrySurface {
13
15
  registerConnector(
@@ -18,15 +20,27 @@ export interface ConnectorRegistrySurface {
18
20
  >,
19
21
  ): void;
20
22
  unregisterConnector(name: string): void;
23
+ registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;
21
24
  }
22
25
 
23
- export interface ConnectorRestPluginOptions extends RestConnectorOptions {}
26
+ /**
27
+ * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no
28
+ * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack
29
+ * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`
30
+ * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097
31
+ * usage.
32
+ */
33
+ export interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {}
24
34
 
25
35
  /**
26
- * ConnectorRestPlugin — registers a generic REST connector on the automation
27
- * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):
28
- * the dispatch node + registry are baseline; a connector like this one is a
29
- * plugin that populates the registry.
36
+ * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018
37
+ * §Addendum) in two forms:
38
+ *
39
+ * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the
40
+ * automation service can materialize declarative `provider: 'rest'`
41
+ * `connectors:` entries into live connectors at boot.
42
+ * 2. **Hand-wired instance** (optional, back-compat): when constructed with a
43
+ * `baseUrl`, it also registers one concrete `rest` connector at `start()`.
30
44
  *
31
45
  * If no automation engine is present the plugin logs and skips — the connector
32
46
  * has nowhere to register, which is not an error.
@@ -35,36 +49,43 @@ export class ConnectorRestPlugin implements Plugin {
35
49
  name = 'com.objectstack.connector.rest';
36
50
  version = '1.0.0';
37
51
  type = 'standard' as const;
38
- // Ensure the automation engine (and its connector registry) is started first.
52
+ // Ensure the automation engine (and its connector/provider registries) exist first.
39
53
  dependencies = ['com.objectstack.service-automation'];
40
54
 
41
55
  private readonly options: ConnectorRestPluginOptions;
42
56
  private connectorName?: string;
43
57
  private automation?: ConnectorRegistrySurface;
44
58
 
45
- constructor(options: ConnectorRestPluginOptions) {
59
+ constructor(options: ConnectorRestPluginOptions = {}) {
46
60
  this.options = options;
47
61
  }
48
62
 
49
- async init(_ctx: PluginContext): Promise<void> {
50
- // No services to register; the connector is registered in start() once
51
- // the automation engine is available.
63
+ async init(ctx: PluginContext): Promise<void> {
64
+ // Contribute the `rest` provider factory (ADR-0097). Done in init() — not
65
+ // start() — so it is registered before the automation service materializes
66
+ // declarative instances during its own start().
67
+ const automation = this.tryGetAutomation(ctx);
68
+ if (automation && typeof automation.registerConnectorProvider === 'function') {
69
+ automation.registerConnectorProvider(
70
+ REST_PROVIDER_KEY,
71
+ createRestProviderFactory({ fetchImpl: this.options.fetchImpl }),
72
+ );
73
+ ctx.logger.info("ConnectorRestPlugin: registered 'rest' connector provider");
74
+ }
52
75
  }
53
76
 
54
77
  async start(ctx: PluginContext): Promise<void> {
55
- let automation: ConnectorRegistrySurface | undefined;
56
- try {
57
- automation = ctx.getService<ConnectorRegistrySurface>('automation');
58
- } catch {
59
- automation = undefined;
60
- }
78
+ // Only register a concrete connector when a baseUrl was supplied — the
79
+ // provider-only usage (no options) contributes just the factory in init().
80
+ if (!this.options.baseUrl) return;
61
81
 
82
+ const automation = this.tryGetAutomation(ctx);
62
83
  if (!automation || typeof automation.registerConnector !== 'function') {
63
84
  ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');
64
85
  return;
65
86
  }
66
87
 
67
- const { def, handlers } = createRestConnector(this.options);
88
+ const { def, handlers } = createRestConnector(this.options as RestConnectorOptions);
68
89
  automation.registerConnector(def, handlers);
69
90
  this.automation = automation;
70
91
  this.connectorName = def.name;
@@ -76,4 +97,12 @@ export class ConnectorRestPlugin implements Plugin {
76
97
  try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }
77
98
  }
78
99
  }
100
+
101
+ private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {
102
+ try {
103
+ return ctx.getService<ConnectorRegistrySurface>('automation');
104
+ } catch {
105
+ return undefined;
106
+ }
107
+ }
79
108
  }
package/src/index.ts CHANGED
@@ -24,3 +24,8 @@ export {
24
24
  type ConnectorRestPluginOptions,
25
25
  type ConnectorRegistrySurface,
26
26
  } from './connector-rest-plugin.js';
27
+ export {
28
+ createRestProviderFactory,
29
+ REST_PROVIDER_KEY,
30
+ type RestProviderDeps,
31
+ } from './rest-provider.js';
@@ -0,0 +1,61 @@
1
+ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2
+ //
3
+ // ADR-0097 — the `rest` provider factory: materialize a declarative
4
+ // `provider: 'rest'` connector instance from providerConfig + resolved auth.
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import type { ConnectorProviderContext } from '@objectstack/spec/integration';
8
+ import { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';
9
+
10
+ function stubFetch() {
11
+ const calls: Array<{ url: string; init: RequestInit }> = [];
12
+ const impl = (async (url: string, init: RequestInit) => {
13
+ calls.push({ url, init });
14
+ return {
15
+ status: 200,
16
+ ok: true,
17
+ headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
18
+ json: async () => ({ ok: true }),
19
+ text: async () => '{}',
20
+ };
21
+ }) as unknown as typeof fetch;
22
+ return { impl, calls };
23
+ }
24
+
25
+ function ctx(partial: Partial<ConnectorProviderContext> & Pick<ConnectorProviderContext, 'providerConfig'>): ConnectorProviderContext {
26
+ return { name: 'svc', label: 'Svc', type: 'api', ...partial };
27
+ }
28
+
29
+ describe('rest provider factory (ADR-0097)', () => {
30
+ it('advertises the rest provider key', () => {
31
+ expect(REST_PROVIDER_KEY).toBe('rest');
32
+ });
33
+
34
+ it('builds a def + request handler from providerConfig.baseUrl and applies resolved auth', async () => {
35
+ const { impl, calls } = stubFetch();
36
+ const factory = createRestProviderFactory({ fetchImpl: impl });
37
+ const { def, handlers } = await factory(
38
+ ctx({ name: 'billing', label: 'Billing', providerConfig: { baseUrl: 'https://api.example.com' }, auth: { type: 'bearer', token: 'tok' } }),
39
+ );
40
+
41
+ expect(def.name).toBe('billing');
42
+ expect(Object.keys(handlers)).toEqual(['request']);
43
+
44
+ const out = await handlers.request({ path: '/ping' }, {});
45
+ expect(out).toMatchObject({ status: 200, ok: true });
46
+ expect(calls[0].url).toBe('https://api.example.com/ping');
47
+ expect((calls[0].init.headers as Record<string, string>).Authorization).toBe('Bearer tok');
48
+ });
49
+
50
+ it('throws when providerConfig.baseUrl is missing', () => {
51
+ const factory = createRestProviderFactory();
52
+ expect(() => factory(ctx({ providerConfig: {} }))).toThrow(/baseUrl/);
53
+ });
54
+
55
+ it('throws when providerConfig.defaultHeaders is not a string map', () => {
56
+ const factory = createRestProviderFactory();
57
+ expect(() =>
58
+ factory(ctx({ providerConfig: { baseUrl: 'https://x', defaultHeaders: { n: 1 } } })),
59
+ ).toThrow(/defaultHeaders/);
60
+ });
61
+ });
@@ -0,0 +1,64 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';
4
+ import { createRestConnector, type RestAuth } from './rest-connector.js';
5
+
6
+ /**
7
+ * The provider key this package contributes (ADR-0097). A declarative
8
+ * `connectors:` entry with `provider: 'rest'` is materialized by this factory.
9
+ */
10
+ export const REST_PROVIDER_KEY = 'rest';
11
+
12
+ /** Injectable dependencies for {@link createRestProviderFactory} (tests). */
13
+ export interface RestProviderDeps {
14
+ /** Injected fetch implementation; defaults to the global `fetch`. */
15
+ fetchImpl?: typeof fetch;
16
+ }
17
+
18
+ /** Shape of `providerConfig` for a `provider: 'rest'` declarative instance. */
19
+ interface RestProviderConfig {
20
+ baseUrl?: unknown;
21
+ defaultHeaders?: unknown;
22
+ }
23
+
24
+ function isStringRecord(v: unknown): v is Record<string, string> {
25
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
26
+ return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');
27
+ }
28
+
29
+ /**
30
+ * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the
31
+ * automation service invokes it for each `provider: 'rest'` declarative instance,
32
+ * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same
33
+ * `{ def, handlers }` bundle {@link createRestConnector} produces for a
34
+ * hand-wired REST connector — one `request` action over static-auth HTTP.
35
+ *
36
+ * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance
37
+ * fails boot loudly rather than materializing a dead connector.
38
+ */
39
+ export function createRestProviderFactory(deps: RestProviderDeps = {}): ConnectorProviderFactory {
40
+ return (ctx) => {
41
+ const cfg = (ctx.providerConfig ?? {}) as RestProviderConfig;
42
+ if (typeof cfg.baseUrl !== 'string' || cfg.baseUrl.length === 0) {
43
+ throw new Error(
44
+ `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`,
45
+ );
46
+ }
47
+ if (cfg.defaultHeaders !== undefined && !isStringRecord(cfg.defaultHeaders)) {
48
+ throw new Error(
49
+ `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string→string map.`,
50
+ );
51
+ }
52
+ // `ResolvedConnectorAuth` is exactly the static-auth subset RestAuth expects
53
+ // (the credentialRef has already been resolved to a secret upstream).
54
+ const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;
55
+ return createRestConnector({
56
+ name: ctx.name,
57
+ label: ctx.label,
58
+ baseUrl: cfg.baseUrl,
59
+ auth,
60
+ defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : undefined,
61
+ fetchImpl: deps.fetchImpl,
62
+ });
63
+ };
64
+ }