@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +88 -0
- package/dist/index.d.mts +47 -11
- package/dist/index.d.ts +47 -11
- package/dist/index.js +57 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +54 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/connector-rest-plugin.test.ts +71 -0
- package/src/connector-rest-plugin.ts +49 -20
- package/src/index.ts +5 -0
- package/src/rest-provider.test.ts +61 -0
- package/src/rest-provider.ts +64 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/connector-rest@15.
|
|
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
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m6.23 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m18.35 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 75ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m7.42 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m19.43 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 77ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 13889ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m5.43 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m5.43 KB[39m
|
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
|
|
52
|
-
* needs no runtime dependency on
|
|
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
|
-
|
|
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 —
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
78
|
-
init(
|
|
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
|
-
|
|
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
|
|
52
|
-
* needs no runtime dependency on
|
|
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
|
-
|
|
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 —
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
78
|
-
init(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
179
|
+
REST_PROVIDER_KEY,
|
|
180
|
+
createRestConnector,
|
|
181
|
+
createRestProviderFactory
|
|
138
182
|
};
|
|
139
183
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\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.
|
|
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.
|
|
17
|
-
"@objectstack/spec": "15.
|
|
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.
|
|
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
|
|
10
|
-
* needs no runtime dependency on
|
|
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
|
-
|
|
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 —
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
|
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(
|
|
50
|
-
//
|
|
51
|
-
// the automation
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
@@ -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
|
+
}
|