@objectstack/connector-rest 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +93 -0
- package/dist/index.d.mts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +139 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -0
- package/src/connector-rest-plugin.test.ts +83 -0
- package/src/connector-rest-plugin.ts +79 -0
- package/src/index.ts +26 -0
- package/src/rest-connector.test.ts +139 -0
- package/src/rest-connector.ts +174 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
> @objectstack/connector-rest@7.4.0 build /home/runner/work/framework/framework/packages/connectors/connector-rest
|
|
3
|
+
> tsup --config ../../../tsup.config.ts
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: es2020
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
[34mCJS[39m Build start
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m4.58 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m12.89 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 69ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.65 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m13.84 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 94ms
|
|
19
|
+
[34mDTS[39m Build start
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 13989ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m3.62 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m3.62 KB[39m
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @objectstack/connector-rest
|
|
2
|
+
|
|
3
|
+
## 7.4.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [23c7107]
|
|
8
|
+
- Updated dependencies [c72daad]
|
|
9
|
+
- Updated dependencies [f115182]
|
|
10
|
+
- Updated dependencies [2faf9f2]
|
|
11
|
+
- Updated dependencies [2faf9f2]
|
|
12
|
+
- Updated dependencies [2faf9f2]
|
|
13
|
+
- Updated dependencies [58b450b]
|
|
14
|
+
- Updated dependencies [82eb6cf]
|
|
15
|
+
- Updated dependencies [13d8653]
|
|
16
|
+
- Updated dependencies [ff3d006]
|
|
17
|
+
- Updated dependencies [5e831de]
|
|
18
|
+
- @objectstack/spec@7.4.0
|
|
19
|
+
- @objectstack/core@7.4.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved.
|
|
2
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
3
|
+
|
|
4
|
+
Parameters
|
|
5
|
+
|
|
6
|
+
Licensor: ObjectStack AI LLC
|
|
7
|
+
Licensed Work: ObjectStack Runtime: the BSL-licensed packages
|
|
8
|
+
of the ObjectStack monorepo as listed in LICENSING.md.
|
|
9
|
+
Copyright (c) 2026 ObjectStack AI LLC.
|
|
10
|
+
Additional Use Grant: You may make production use of the Licensed Work, provided
|
|
11
|
+
Your use does not include offering the Licensed Work to third
|
|
12
|
+
parties on a hosted or embedded basis in order to compete with
|
|
13
|
+
ObjectStack AI LLC's paid version(s) of the Licensed Work. For purposes
|
|
14
|
+
of this license:
|
|
15
|
+
|
|
16
|
+
A "competitive offering" is a Product that is offered to third
|
|
17
|
+
parties on a paid basis, including through paid support
|
|
18
|
+
arrangements, that significantly overlaps with the capabilities
|
|
19
|
+
of ObjectStack AI LLC's paid version(s) of the Licensed Work. If Your
|
|
20
|
+
Product is not a competitive offering when You first make it
|
|
21
|
+
generally available, it will not become a competitive offering
|
|
22
|
+
later due to ObjectStack AI LLC releasing a new version of the Licensed
|
|
23
|
+
Work with additional capabilities. In addition, Products that
|
|
24
|
+
are not provided on a paid basis are not competitive.
|
|
25
|
+
|
|
26
|
+
"Product" means software that is offered to end users to manage
|
|
27
|
+
in their own environments or offered as a service on a hosted
|
|
28
|
+
basis.
|
|
29
|
+
|
|
30
|
+
"Embedded" means including the source code or executable code
|
|
31
|
+
from the Licensed Work in a competitive offering. "Embedded"
|
|
32
|
+
also means packaging the competitive offering in such a way
|
|
33
|
+
that the Licensed Work must be accessed or downloaded for the
|
|
34
|
+
competitive offering to operate.
|
|
35
|
+
|
|
36
|
+
Hosting or using the Licensed Work(s) for internal purposes
|
|
37
|
+
within an organization is not considered a competitive
|
|
38
|
+
offering. ObjectStack AI LLC considers your organization to include all
|
|
39
|
+
of your affiliates under common control.
|
|
40
|
+
|
|
41
|
+
For binding interpretive guidance on using ObjectStack AI LLC products
|
|
42
|
+
under the Business Source License, please visit our FAQ.
|
|
43
|
+
(see LICENSING.md in this repository)
|
|
44
|
+
Change Date: Four years from the date the Licensed Work is published.
|
|
45
|
+
Change License: Apache License, Version 2.0
|
|
46
|
+
|
|
47
|
+
For information about alternative licensing arrangements for the Licensed Work,
|
|
48
|
+
please contact licensing@objectstack.dev.
|
|
49
|
+
|
|
50
|
+
Notice
|
|
51
|
+
|
|
52
|
+
Business Source License 1.1
|
|
53
|
+
|
|
54
|
+
Terms
|
|
55
|
+
|
|
56
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
57
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
58
|
+
Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
59
|
+
|
|
60
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
61
|
+
available distribution of a specific version of the Licensed Work under this
|
|
62
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
63
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
64
|
+
above terminate.
|
|
65
|
+
|
|
66
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
67
|
+
currently in effect as described in this License, you must purchase a
|
|
68
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
69
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
70
|
+
|
|
71
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
72
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
73
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
74
|
+
for each version of the Licensed Work released by Licensor.
|
|
75
|
+
|
|
76
|
+
You must conspicuously display this License on each original or modified copy
|
|
77
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
78
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
79
|
+
License apply to your use of that work.
|
|
80
|
+
|
|
81
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
82
|
+
terminate your rights under this License for the current and all other
|
|
83
|
+
versions of the Licensed Work.
|
|
84
|
+
|
|
85
|
+
This License does not grant you any right in any trademark or logo of
|
|
86
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
87
|
+
Licensor as expressly required by this License).
|
|
88
|
+
|
|
89
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
90
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
91
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
92
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
93
|
+
TITLE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Connector } from '@objectstack/spec/integration';
|
|
2
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic REST connector — the reference *concrete* connector (ADR-0018
|
|
6
|
+
* §Addendum). It produces a {@link Connector} definition plus the handler for
|
|
7
|
+
* its one action, `request`, which the baseline `connector_action` node
|
|
8
|
+
* dispatches to.
|
|
9
|
+
*
|
|
10
|
+
* Open-source scope: **static** auth only (`none` / `api-key` / `basic` /
|
|
11
|
+
* `bearer`), with credentials supplied by the caller. OAuth2 token acquisition
|
|
12
|
+
* and refresh, credential vaulting, and multi-tenant connection lifecycle are
|
|
13
|
+
* the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are
|
|
14
|
+
* deliberately out of scope here.
|
|
15
|
+
*/
|
|
16
|
+
/** Auth config understood by the REST connector (the static subset). */
|
|
17
|
+
type RestAuth = Extract<Connector['authentication'], {
|
|
18
|
+
type: 'none' | 'api-key' | 'basic' | 'bearer';
|
|
19
|
+
}>;
|
|
20
|
+
interface RestConnectorOptions {
|
|
21
|
+
/** Connector machine name (snake_case). Defaults to `rest`. */
|
|
22
|
+
name?: string;
|
|
23
|
+
/** Human-readable label. Defaults to a title derived from `name`. */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/** Static authentication. Defaults to `{ type: 'none' }`. */
|
|
28
|
+
auth?: RestAuth;
|
|
29
|
+
/** Headers merged into every request (request-level headers win). */
|
|
30
|
+
defaultHeaders?: Record<string, string>;
|
|
31
|
+
/** Injected for tests; defaults to the global `fetch`. */
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
}
|
|
34
|
+
/** Input accepted by the `request` action. */
|
|
35
|
+
interface RestRequestInput {
|
|
36
|
+
method?: string;
|
|
37
|
+
path?: string;
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
40
|
+
body?: unknown;
|
|
41
|
+
}
|
|
42
|
+
/** A connector definition paired with its action handlers, ready for registerConnector(). */
|
|
43
|
+
interface RestConnectorBundle {
|
|
44
|
+
def: Connector;
|
|
45
|
+
handlers: Record<string, (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>>;
|
|
46
|
+
}
|
|
47
|
+
declare function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle;
|
|
48
|
+
|
|
49
|
+
/**
|
|
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`.
|
|
53
|
+
*/
|
|
54
|
+
interface ConnectorRegistrySurface {
|
|
55
|
+
registerConnector(def: Connector, handlers: Record<string, (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>>): void;
|
|
56
|
+
unregisterConnector(name: string): void;
|
|
57
|
+
}
|
|
58
|
+
interface ConnectorRestPluginOptions extends RestConnectorOptions {
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
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.
|
|
65
|
+
*
|
|
66
|
+
* If no automation engine is present the plugin logs and skips — the connector
|
|
67
|
+
* has nowhere to register, which is not an error.
|
|
68
|
+
*/
|
|
69
|
+
declare class ConnectorRestPlugin implements Plugin {
|
|
70
|
+
name: string;
|
|
71
|
+
version: string;
|
|
72
|
+
type: "standard";
|
|
73
|
+
dependencies: string[];
|
|
74
|
+
private readonly options;
|
|
75
|
+
private connectorName?;
|
|
76
|
+
private automation?;
|
|
77
|
+
constructor(options: ConnectorRestPluginOptions);
|
|
78
|
+
init(_ctx: PluginContext): Promise<void>;
|
|
79
|
+
start(ctx: PluginContext): Promise<void>;
|
|
80
|
+
stop(_ctx: PluginContext): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { type ConnectorRegistrySurface, ConnectorRestPlugin, type ConnectorRestPluginOptions, type RestAuth, type RestConnectorBundle, type RestConnectorOptions, type RestRequestInput, createRestConnector };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Connector } from '@objectstack/spec/integration';
|
|
2
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic REST connector — the reference *concrete* connector (ADR-0018
|
|
6
|
+
* §Addendum). It produces a {@link Connector} definition plus the handler for
|
|
7
|
+
* its one action, `request`, which the baseline `connector_action` node
|
|
8
|
+
* dispatches to.
|
|
9
|
+
*
|
|
10
|
+
* Open-source scope: **static** auth only (`none` / `api-key` / `basic` /
|
|
11
|
+
* `bearer`), with credentials supplied by the caller. OAuth2 token acquisition
|
|
12
|
+
* and refresh, credential vaulting, and multi-tenant connection lifecycle are
|
|
13
|
+
* the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are
|
|
14
|
+
* deliberately out of scope here.
|
|
15
|
+
*/
|
|
16
|
+
/** Auth config understood by the REST connector (the static subset). */
|
|
17
|
+
type RestAuth = Extract<Connector['authentication'], {
|
|
18
|
+
type: 'none' | 'api-key' | 'basic' | 'bearer';
|
|
19
|
+
}>;
|
|
20
|
+
interface RestConnectorOptions {
|
|
21
|
+
/** Connector machine name (snake_case). Defaults to `rest`. */
|
|
22
|
+
name?: string;
|
|
23
|
+
/** Human-readable label. Defaults to a title derived from `name`. */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/** Static authentication. Defaults to `{ type: 'none' }`. */
|
|
28
|
+
auth?: RestAuth;
|
|
29
|
+
/** Headers merged into every request (request-level headers win). */
|
|
30
|
+
defaultHeaders?: Record<string, string>;
|
|
31
|
+
/** Injected for tests; defaults to the global `fetch`. */
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
}
|
|
34
|
+
/** Input accepted by the `request` action. */
|
|
35
|
+
interface RestRequestInput {
|
|
36
|
+
method?: string;
|
|
37
|
+
path?: string;
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
40
|
+
body?: unknown;
|
|
41
|
+
}
|
|
42
|
+
/** A connector definition paired with its action handlers, ready for registerConnector(). */
|
|
43
|
+
interface RestConnectorBundle {
|
|
44
|
+
def: Connector;
|
|
45
|
+
handlers: Record<string, (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>>;
|
|
46
|
+
}
|
|
47
|
+
declare function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle;
|
|
48
|
+
|
|
49
|
+
/**
|
|
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`.
|
|
53
|
+
*/
|
|
54
|
+
interface ConnectorRegistrySurface {
|
|
55
|
+
registerConnector(def: Connector, handlers: Record<string, (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>>): void;
|
|
56
|
+
unregisterConnector(name: string): void;
|
|
57
|
+
}
|
|
58
|
+
interface ConnectorRestPluginOptions extends RestConnectorOptions {
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
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.
|
|
65
|
+
*
|
|
66
|
+
* If no automation engine is present the plugin logs and skips — the connector
|
|
67
|
+
* has nowhere to register, which is not an error.
|
|
68
|
+
*/
|
|
69
|
+
declare class ConnectorRestPlugin implements Plugin {
|
|
70
|
+
name: string;
|
|
71
|
+
version: string;
|
|
72
|
+
type: "standard";
|
|
73
|
+
dependencies: string[];
|
|
74
|
+
private readonly options;
|
|
75
|
+
private connectorName?;
|
|
76
|
+
private automation?;
|
|
77
|
+
constructor(options: ConnectorRestPluginOptions);
|
|
78
|
+
init(_ctx: PluginContext): Promise<void>;
|
|
79
|
+
start(ctx: PluginContext): Promise<void>;
|
|
80
|
+
stop(_ctx: PluginContext): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { type ConnectorRegistrySurface, ConnectorRestPlugin, type ConnectorRestPluginOptions, type RestAuth, type RestConnectorBundle, type RestConnectorOptions, type RestRequestInput, createRestConnector };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ConnectorRestPlugin: () => ConnectorRestPlugin,
|
|
24
|
+
createRestConnector: () => createRestConnector
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/rest-connector.ts
|
|
29
|
+
function buildUrl(baseUrl, path, query) {
|
|
30
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
31
|
+
const suffix = path ? path.startsWith("/") ? path : `/${path}` : "";
|
|
32
|
+
const url = new URL(base + suffix);
|
|
33
|
+
if (query) {
|
|
34
|
+
for (const [key, value] of Object.entries(query)) {
|
|
35
|
+
if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
function applyAuth(auth, headers, query) {
|
|
41
|
+
switch (auth.type) {
|
|
42
|
+
case "none":
|
|
43
|
+
return;
|
|
44
|
+
case "bearer":
|
|
45
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
46
|
+
return;
|
|
47
|
+
case "basic": {
|
|
48
|
+
const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
|
|
49
|
+
headers["Authorization"] = `Basic ${encoded}`;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
case "api-key":
|
|
53
|
+
if (auth.paramName) query[auth.paramName] = auth.key;
|
|
54
|
+
else headers[auth.headerName ?? "X-API-Key"] = auth.key;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function createRestConnector(opts) {
|
|
59
|
+
const name = opts.name ?? "rest";
|
|
60
|
+
const auth = opts.auth ?? { type: "none" };
|
|
61
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
62
|
+
const def = {
|
|
63
|
+
name,
|
|
64
|
+
label: opts.label ?? "REST Connector",
|
|
65
|
+
type: "api",
|
|
66
|
+
description: "Generic REST/HTTP connector with static authentication.",
|
|
67
|
+
icon: "globe",
|
|
68
|
+
authentication: auth,
|
|
69
|
+
// Defaulted by ConnectorSchema; set explicitly so the literal satisfies
|
|
70
|
+
// the (post-parse) Connector output type.
|
|
71
|
+
status: "active",
|
|
72
|
+
enabled: true,
|
|
73
|
+
connectionTimeoutMs: 3e4,
|
|
74
|
+
requestTimeoutMs: 3e4,
|
|
75
|
+
actions: [
|
|
76
|
+
{
|
|
77
|
+
key: "request",
|
|
78
|
+
label: "HTTP Request",
|
|
79
|
+
description: "Send an HTTP request to the connector's base URL with static auth applied.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
method: { type: "string", description: "HTTP method (default GET)" },
|
|
84
|
+
path: { type: "string", description: "Path appended to the base URL" },
|
|
85
|
+
headers: { type: "object", description: "Per-request headers" },
|
|
86
|
+
query: { type: "object", description: "Query parameters" },
|
|
87
|
+
body: { description: "Request body (JSON-encoded for non-GET)" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
outputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
status: { type: "number" },
|
|
94
|
+
ok: { type: "boolean" },
|
|
95
|
+
body: {}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
async function request(input) {
|
|
102
|
+
const req = input;
|
|
103
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
104
|
+
const headers = { ...opts.defaultHeaders, ...req.headers };
|
|
105
|
+
const query = { ...req.query };
|
|
106
|
+
applyAuth(auth, headers, query);
|
|
107
|
+
const url = buildUrl(opts.baseUrl, req.path ?? "", query);
|
|
108
|
+
const hasBody = req.body !== void 0 && method !== "GET" && method !== "HEAD";
|
|
109
|
+
if (hasBody && headers["Content-Type"] === void 0 && headers["content-type"] === void 0) {
|
|
110
|
+
headers["Content-Type"] = "application/json";
|
|
111
|
+
}
|
|
112
|
+
const response = await doFetch(url, {
|
|
113
|
+
method,
|
|
114
|
+
headers,
|
|
115
|
+
body: hasBody ? JSON.stringify(req.body) : void 0
|
|
116
|
+
});
|
|
117
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
118
|
+
const parsed = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
119
|
+
return { status: response.status, ok: response.ok, body: parsed };
|
|
120
|
+
}
|
|
121
|
+
return { def, handlers: { request } };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/connector-rest-plugin.ts
|
|
125
|
+
var ConnectorRestPlugin = class {
|
|
126
|
+
constructor(options) {
|
|
127
|
+
this.name = "com.objectstack.connector.rest";
|
|
128
|
+
this.version = "1.0.0";
|
|
129
|
+
this.type = "standard";
|
|
130
|
+
// Ensure the automation engine (and its connector registry) is started first.
|
|
131
|
+
this.dependencies = ["com.objectstack.service-automation"];
|
|
132
|
+
this.options = options;
|
|
133
|
+
}
|
|
134
|
+
async init(_ctx) {
|
|
135
|
+
}
|
|
136
|
+
async start(ctx) {
|
|
137
|
+
let automation;
|
|
138
|
+
try {
|
|
139
|
+
automation = ctx.getService("automation");
|
|
140
|
+
} catch {
|
|
141
|
+
automation = void 0;
|
|
142
|
+
}
|
|
143
|
+
if (!automation || typeof automation.registerConnector !== "function") {
|
|
144
|
+
ctx.logger.info("ConnectorRestPlugin: no automation engine \u2014 REST connector not registered");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const { def, handlers } = createRestConnector(this.options);
|
|
148
|
+
automation.registerConnector(def, handlers);
|
|
149
|
+
this.automation = automation;
|
|
150
|
+
this.connectorName = def.name;
|
|
151
|
+
ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);
|
|
152
|
+
}
|
|
153
|
+
async stop(_ctx) {
|
|
154
|
+
if (this.automation && this.connectorName) {
|
|
155
|
+
try {
|
|
156
|
+
this.automation.unregisterConnector(this.connectorName);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
163
|
+
0 && (module.exports = {
|
|
164
|
+
ConnectorRestPlugin,
|
|
165
|
+
createRestConnector
|
|
166
|
+
});
|
|
167
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-rest\n *\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). The baseline automation engine ships the `connector_action`\n * dispatch node + an empty connector registry; this plugin populates the\n * registry with a `rest` connector exposing a `request` action.\n *\n * Static auth only (`none` / `api-key` / `basic` / `bearer`); OAuth2 refresh,\n * credential vaulting, and multi-tenant lifecycle are the enterprise tier.\n */\n\nexport {\n createRestConnector,\n type RestConnectorOptions,\n type RestConnectorBundle,\n type RestRequestInput,\n type RestAuth,\n} from './rest-connector.js';\nexport {\n ConnectorRestPlugin,\n type ConnectorRestPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-rest-plugin.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n const doFetch = opts.fetchImpl ?? fetch;\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACyDA,SAAS,SAAS,SAAiB,MAAc,OAA2C;AACxF,QAAM,OAAO,QAAQ,QAAQ,QAAQ,EAAE;AACvC,QAAM,SAAS,OAAQ,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI,KAAM;AACnE,QAAM,MAAM,IAAI,IAAI,OAAO,MAAM;AACjC,MAAI,OAAO;AACP,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC9C,UAAI,UAAU,UAAa,UAAU,KAAM,KAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IACtF;AAAA,EACJ;AACA,SAAO,IAAI,SAAS;AACxB;AAMA,SAAS,UACL,MACA,SACA,OACI;AACJ,UAAQ,KAAK,MAAM;AAAA,IACf,KAAK;AACD;AAAA,IACJ,KAAK;AACD,cAAQ,eAAe,IAAI,UAAU,KAAK,KAAK;AAC/C;AAAA,IACJ,KAAK,SAAS;AACV,YAAM,UAAU,OAAO,KAAK,GAAG,KAAK,QAAQ,IAAI,KAAK,QAAQ,EAAE,EAAE,SAAS,QAAQ;AAClF,cAAQ,eAAe,IAAI,SAAS,OAAO;AAC3C;AAAA,IACJ;AAAA,IACA,KAAK;AACD,UAAI,KAAK,UAAW,OAAM,KAAK,SAAS,IAAI,KAAK;AAAA,UAC5C,SAAQ,KAAK,cAAc,WAAW,IAAI,KAAK;AACpD;AAAA,EACR;AACJ;AAEO,SAAS,oBAAoB,MAAiD;AACjF,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,OAAiB,KAAK,QAAQ,EAAE,MAAM,OAAO;AACnD,QAAM,UAAU,KAAK,aAAa;AAElC,QAAM,MAAiB;AAAA,IACnB;AAAA,IACA,OAAO,KAAK,SAAS;AAAA,IACrB,MAAM;AAAA,IACN,aAAa;AAAA,IACb,MAAM;AAAA,IACN,gBAAgB;AAAA;AAAA;AAAA,IAGhB,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,qBAAqB;AAAA,IACrB,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACL;AAAA,QACI,KAAK;AAAA,QACL,OAAO;AAAA,QACP,aAAa;AAAA,QACb,aAAa;AAAA,UACT,MAAM;AAAA,UACN,YAAY;AAAA,YACR,QAAQ,EAAE,MAAM,UAAU,aAAa,4BAA4B;AAAA,YACnE,MAAM,EAAE,MAAM,UAAU,aAAa,gCAAgC;AAAA,YACrE,SAAS,EAAE,MAAM,UAAU,aAAa,sBAAsB;AAAA,YAC9D,OAAO,EAAE,MAAM,UAAU,aAAa,mBAAmB;AAAA,YACzD,MAAM,EAAE,aAAa,0CAA0C;AAAA,UACnE;AAAA,QACJ;AAAA,QACA,cAAc;AAAA,UACV,MAAM;AAAA,UACN,YAAY;AAAA,YACR,QAAQ,EAAE,MAAM,SAAS;AAAA,YACzB,IAAI,EAAE,MAAM,UAAU;AAAA,YACtB,MAAM,CAAC;AAAA,UACX;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAEA,iBAAe,QAAQ,OAAkE;AACrF,UAAM,MAAM;AACZ,UAAM,UAAU,IAAI,UAAU,OAAO,YAAY;AACjD,UAAM,UAAkC,EAAE,GAAG,KAAK,gBAAgB,GAAG,IAAI,QAAQ;AACjF,UAAM,QAAsE,EAAE,GAAG,IAAI,MAAM;AAE3F,cAAU,MAAM,SAAS,KAAK;AAE9B,UAAM,MAAM,SAAS,KAAK,SAAS,IAAI,QAAQ,IAAI,KAAK;AAExD,UAAM,UAAU,IAAI,SAAS,UAAa,WAAW,SAAS,WAAW;AACzE,QAAI,WAAW,QAAQ,cAAc,MAAM,UAAa,QAAQ,cAAc,MAAM,QAAW;AAC3F,cAAQ,cAAc,IAAI;AAAA,IAC9B;AAEA,UAAM,WAAW,MAAM,QAAQ,KAAK;AAAA,MAChC;AAAA,MACA;AAAA,MACA,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,IAC/C,CAAC;AAID,UAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,UAAM,SAAS,YAAY,SAAS,kBAAkB,IAChD,MAAM,SAAS,KAAK,IACpB,MAAM,SAAS,KAAK;AAE1B,WAAO,EAAE,QAAQ,SAAS,QAAQ,IAAI,SAAS,IAAI,MAAM,OAAO;AAAA,EACpE;AAEA,SAAO,EAAE,KAAK,UAAU,EAAE,QAAQ,EAAE;AACxC;;;AC5IO,IAAM,sBAAN,MAA4C;AAAA,EAW/C,YAAY,SAAqC;AAVjD,gBAAO;AACP,mBAAU;AACV,gBAAO;AAEP;AAAA,wBAAe,CAAC,oCAAoC;AAOhD,SAAK,UAAU;AAAA,EACnB;AAAA,EAEA,MAAM,KAAK,MAAoC;AAAA,EAG/C;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC3C,QAAI;AACJ,QAAI;AACA,mBAAa,IAAI,WAAqC,YAAY;AAAA,IACtE,QAAQ;AACJ,mBAAa;AAAA,IACjB;AAEA,QAAI,CAAC,cAAc,OAAO,WAAW,sBAAsB,YAAY;AACnE,UAAI,OAAO,KAAK,gFAA2E;AAC3F;AAAA,IACJ;AAEA,UAAM,EAAE,KAAK,SAAS,IAAI,oBAAoB,KAAK,OAAO;AAC1D,eAAW,kBAAkB,KAAK,QAAQ;AAC1C,SAAK,aAAa;AAClB,SAAK,gBAAgB,IAAI;AACzB,QAAI,OAAO,KAAK,wCAAwC,IAAI,IAAI,cAAc;AAAA,EAClF;AAAA,EAEA,MAAM,KAAK,MAAoC;AAC3C,QAAI,KAAK,cAAc,KAAK,eAAe;AACvC,UAAI;AAAE,aAAK,WAAW,oBAAoB,KAAK,aAAa;AAAA,MAAG,QAAQ;AAAA,MAAe;AAAA,IAC1F;AAAA,EACJ;AACJ;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/rest-connector.ts
|
|
2
|
+
function buildUrl(baseUrl, path, query) {
|
|
3
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
4
|
+
const suffix = path ? path.startsWith("/") ? path : `/${path}` : "";
|
|
5
|
+
const url = new URL(base + suffix);
|
|
6
|
+
if (query) {
|
|
7
|
+
for (const [key, value] of Object.entries(query)) {
|
|
8
|
+
if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return url.toString();
|
|
12
|
+
}
|
|
13
|
+
function applyAuth(auth, headers, query) {
|
|
14
|
+
switch (auth.type) {
|
|
15
|
+
case "none":
|
|
16
|
+
return;
|
|
17
|
+
case "bearer":
|
|
18
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
19
|
+
return;
|
|
20
|
+
case "basic": {
|
|
21
|
+
const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
|
|
22
|
+
headers["Authorization"] = `Basic ${encoded}`;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
case "api-key":
|
|
26
|
+
if (auth.paramName) query[auth.paramName] = auth.key;
|
|
27
|
+
else headers[auth.headerName ?? "X-API-Key"] = auth.key;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createRestConnector(opts) {
|
|
32
|
+
const name = opts.name ?? "rest";
|
|
33
|
+
const auth = opts.auth ?? { type: "none" };
|
|
34
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
35
|
+
const def = {
|
|
36
|
+
name,
|
|
37
|
+
label: opts.label ?? "REST Connector",
|
|
38
|
+
type: "api",
|
|
39
|
+
description: "Generic REST/HTTP connector with static authentication.",
|
|
40
|
+
icon: "globe",
|
|
41
|
+
authentication: auth,
|
|
42
|
+
// Defaulted by ConnectorSchema; set explicitly so the literal satisfies
|
|
43
|
+
// the (post-parse) Connector output type.
|
|
44
|
+
status: "active",
|
|
45
|
+
enabled: true,
|
|
46
|
+
connectionTimeoutMs: 3e4,
|
|
47
|
+
requestTimeoutMs: 3e4,
|
|
48
|
+
actions: [
|
|
49
|
+
{
|
|
50
|
+
key: "request",
|
|
51
|
+
label: "HTTP Request",
|
|
52
|
+
description: "Send an HTTP request to the connector's base URL with static auth applied.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
method: { type: "string", description: "HTTP method (default GET)" },
|
|
57
|
+
path: { type: "string", description: "Path appended to the base URL" },
|
|
58
|
+
headers: { type: "object", description: "Per-request headers" },
|
|
59
|
+
query: { type: "object", description: "Query parameters" },
|
|
60
|
+
body: { description: "Request body (JSON-encoded for non-GET)" }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
outputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
status: { type: "number" },
|
|
67
|
+
ok: { type: "boolean" },
|
|
68
|
+
body: {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
async function request(input) {
|
|
75
|
+
const req = input;
|
|
76
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
77
|
+
const headers = { ...opts.defaultHeaders, ...req.headers };
|
|
78
|
+
const query = { ...req.query };
|
|
79
|
+
applyAuth(auth, headers, query);
|
|
80
|
+
const url = buildUrl(opts.baseUrl, req.path ?? "", query);
|
|
81
|
+
const hasBody = req.body !== void 0 && method !== "GET" && method !== "HEAD";
|
|
82
|
+
if (hasBody && headers["Content-Type"] === void 0 && headers["content-type"] === void 0) {
|
|
83
|
+
headers["Content-Type"] = "application/json";
|
|
84
|
+
}
|
|
85
|
+
const response = await doFetch(url, {
|
|
86
|
+
method,
|
|
87
|
+
headers,
|
|
88
|
+
body: hasBody ? JSON.stringify(req.body) : void 0
|
|
89
|
+
});
|
|
90
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
91
|
+
const parsed = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
92
|
+
return { status: response.status, ok: response.ok, body: parsed };
|
|
93
|
+
}
|
|
94
|
+
return { def, handlers: { request } };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/connector-rest-plugin.ts
|
|
98
|
+
var ConnectorRestPlugin = class {
|
|
99
|
+
constructor(options) {
|
|
100
|
+
this.name = "com.objectstack.connector.rest";
|
|
101
|
+
this.version = "1.0.0";
|
|
102
|
+
this.type = "standard";
|
|
103
|
+
// Ensure the automation engine (and its connector registry) is started first.
|
|
104
|
+
this.dependencies = ["com.objectstack.service-automation"];
|
|
105
|
+
this.options = options;
|
|
106
|
+
}
|
|
107
|
+
async init(_ctx) {
|
|
108
|
+
}
|
|
109
|
+
async start(ctx) {
|
|
110
|
+
let automation;
|
|
111
|
+
try {
|
|
112
|
+
automation = ctx.getService("automation");
|
|
113
|
+
} catch {
|
|
114
|
+
automation = void 0;
|
|
115
|
+
}
|
|
116
|
+
if (!automation || typeof automation.registerConnector !== "function") {
|
|
117
|
+
ctx.logger.info("ConnectorRestPlugin: no automation engine \u2014 REST connector not registered");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { def, handlers } = createRestConnector(this.options);
|
|
121
|
+
automation.registerConnector(def, handlers);
|
|
122
|
+
this.automation = automation;
|
|
123
|
+
this.connectorName = def.name;
|
|
124
|
+
ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);
|
|
125
|
+
}
|
|
126
|
+
async stop(_ctx) {
|
|
127
|
+
if (this.automation && this.connectorName) {
|
|
128
|
+
try {
|
|
129
|
+
this.automation.unregisterConnector(this.connectorName);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
export {
|
|
136
|
+
ConnectorRestPlugin,
|
|
137
|
+
createRestConnector
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rest-connector.ts","../src/connector-rest-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * Generic REST connector — the reference *concrete* connector (ADR-0018\n * §Addendum). It produces a {@link Connector} definition plus the handler for\n * its one action, `request`, which the baseline `connector_action` node\n * dispatches to.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition\n * and refresh, credential vaulting, and multi-tenant connection lifecycle are\n * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are\n * deliberately out of scope here.\n */\n\n/** Auth config understood by the REST connector (the static subset). */\nexport type RestAuth = Extract<\n Connector['authentication'],\n { type: 'none' | 'api-key' | 'basic' | 'bearer' }\n>;\n\nexport interface RestConnectorOptions {\n /** Connector machine name (snake_case). Defaults to `rest`. */\n name?: string;\n /** Human-readable label. Defaults to a title derived from `name`. */\n label?: string;\n /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */\n baseUrl: string;\n /** Static authentication. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Injected for tests; defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Input accepted by the `request` action. */\nexport interface RestRequestInput {\n method?: string;\n path?: string;\n headers?: Record<string, string>;\n query?: Record<string, string | number | boolean | null | undefined>;\n body?: unknown;\n}\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface RestConnectorBundle {\n def: Connector;\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n}\n\n/**\n * Apply static auth to the outgoing headers / query. Returns possibly-extended\n * query so an `api-key` configured with `paramName` can ride the query string.\n */\nfunction applyAuth(\n auth: RestAuth,\n headers: Record<string, string>,\n query: Record<string, string | number | boolean | null | undefined>,\n): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\nexport function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {\n const name = opts.name ?? 'rest';\n const auth: RestAuth = opts.auth ?? { type: 'none' };\n const doFetch = opts.fetchImpl ?? fetch;\n\n const def: Connector = {\n name,\n label: opts.label ?? 'REST Connector',\n type: 'api',\n description: 'Generic REST/HTTP connector with static authentication.',\n icon: 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type.\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions: [\n {\n key: 'request',\n label: 'HTTP Request',\n description: 'Send an HTTP request to the connector\\'s base URL with static auth applied.',\n inputSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', description: 'HTTP method (default GET)' },\n path: { type: 'string', description: 'Path appended to the base URL' },\n headers: { type: 'object', description: 'Per-request headers' },\n query: { type: 'object', description: 'Query parameters' },\n body: { description: 'Request body (JSON-encoded for non-GET)' },\n },\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number' },\n ok: { type: 'boolean' },\n body: {},\n },\n },\n },\n ],\n };\n\n async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {\n const req = input as RestRequestInput;\n const method = (req.method ?? 'GET').toUpperCase();\n const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };\n const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };\n\n applyAuth(auth, headers, query);\n\n const url = buildUrl(opts.baseUrl, req.path ?? '', query);\n\n const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(req.body) : undefined,\n });\n\n // Parse JSON when advertised; fall back to text so non-JSON endpoints\n // don't throw.\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json')\n ? await response.json()\n : await response.text();\n\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n return { def, handlers: { request } };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector } from '@objectstack/spec/integration';\nimport { createRestConnector, type RestConnectorOptions } from './rest-connector.js';\n\n/**\n * Minimal surface of the automation engine this plugin depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so the plugin\n * needs no runtime dependency on `@objectstack/service-automation`.\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n}\n\nexport interface ConnectorRestPluginOptions extends RestConnectorOptions {}\n\n/**\n * ConnectorRestPlugin — registers a generic REST connector on the automation\n * engine. This is the **reference concrete connector** (ADR-0018 §Addendum):\n * the dispatch node + registry are baseline; a connector like this one is a\n * plugin that populates the registry.\n *\n * If no automation engine is present the plugin logs and skips — the connector\n * has nowhere to register, which is not an error.\n */\nexport class ConnectorRestPlugin implements Plugin {\n name = 'com.objectstack.connector.rest';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector registry) is started first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorRestPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorRestPluginOptions) {\n this.options = options;\n }\n\n async init(_ctx: PluginContext): Promise<void> {\n // No services to register; the connector is registered in start() once\n // the automation engine is available.\n }\n\n async start(ctx: PluginContext): Promise<void> {\n let automation: ConnectorRegistrySurface | undefined;\n try {\n automation = ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n automation = undefined;\n }\n\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');\n return;\n }\n\n const { def, handlers } = createRestConnector(this.options);\n automation.registerConnector(def, handlers);\n this.automation = automation;\n this.connectorName = def.name;\n ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectstack/connector-rest",
|
|
3
|
+
"version": "7.4.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
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
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@objectstack/core": "7.4.0",
|
|
17
|
+
"@objectstack/spec": "7.4.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^25.9.1",
|
|
21
|
+
"typescript": "^6.0.3",
|
|
22
|
+
"vitest": "^4.1.7",
|
|
23
|
+
"@objectstack/service-automation": "7.4.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"objectstack",
|
|
27
|
+
"connector",
|
|
28
|
+
"rest",
|
|
29
|
+
"integration",
|
|
30
|
+
"http"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup --config ../../../tsup.config.ts",
|
|
34
|
+
"test": "vitest run --passWithNoTests"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { LiteKernel } from '@objectstack/core';
|
|
5
|
+
import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
|
|
6
|
+
import { ConnectorRestPlugin } from './connector-rest-plugin.js';
|
|
7
|
+
|
|
8
|
+
/** A fetch stub recording calls, returning a fixed JSON response. */
|
|
9
|
+
function stubFetch() {
|
|
10
|
+
const calls: Array<{ url: string; init: RequestInit }> = [];
|
|
11
|
+
const impl = (async (url: string, init: RequestInit) => {
|
|
12
|
+
calls.push({ url, init });
|
|
13
|
+
return {
|
|
14
|
+
status: 201,
|
|
15
|
+
ok: true,
|
|
16
|
+
headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
|
|
17
|
+
json: async () => ({ id: 'created-1' }),
|
|
18
|
+
text: async () => '{"id":"created-1"}',
|
|
19
|
+
};
|
|
20
|
+
}) as unknown as typeof fetch;
|
|
21
|
+
return { impl, calls };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('ConnectorRestPlugin — end to end with the automation engine', () => {
|
|
25
|
+
it('registers the REST connector so a connector_action flow dispatches to it', async () => {
|
|
26
|
+
const { impl, calls } = stubFetch();
|
|
27
|
+
|
|
28
|
+
const kernel = new LiteKernel();
|
|
29
|
+
kernel.use(new AutomationServicePlugin());
|
|
30
|
+
kernel.use(
|
|
31
|
+
new ConnectorRestPlugin({
|
|
32
|
+
baseUrl: 'https://api.example.com',
|
|
33
|
+
auth: { type: 'bearer', token: 'secret-token' },
|
|
34
|
+
fetchImpl: impl,
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
await kernel.bootstrap();
|
|
38
|
+
|
|
39
|
+
const engine = kernel.getService<AutomationEngine>('automation');
|
|
40
|
+
|
|
41
|
+
// The baseline node and the plugin-contributed connector are both present.
|
|
42
|
+
expect(engine.getRegisteredNodeTypes()).toContain('connector_action');
|
|
43
|
+
expect(engine.getRegisteredConnectors()).toContain('rest');
|
|
44
|
+
|
|
45
|
+
engine.registerFlow('create_via_rest', {
|
|
46
|
+
name: 'create_via_rest',
|
|
47
|
+
label: 'Create via REST',
|
|
48
|
+
type: 'autolaunched',
|
|
49
|
+
variables: [{ name: 'call.body', type: 'json', isOutput: true }],
|
|
50
|
+
nodes: [
|
|
51
|
+
{ id: 'start', type: 'start', label: 'Start' },
|
|
52
|
+
{
|
|
53
|
+
id: 'call',
|
|
54
|
+
type: 'connector_action',
|
|
55
|
+
label: 'POST /items',
|
|
56
|
+
connectorConfig: {
|
|
57
|
+
connectorId: 'rest',
|
|
58
|
+
actionId: 'request',
|
|
59
|
+
input: { method: 'POST', path: '/items', body: { name: 'Widget' } },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{ id: 'end', type: 'end', label: 'End' },
|
|
63
|
+
],
|
|
64
|
+
edges: [
|
|
65
|
+
{ id: 'e1', source: 'start', target: 'call' },
|
|
66
|
+
{ id: 'e2', source: 'call', target: 'end' },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await engine.execute('create_via_rest');
|
|
71
|
+
|
|
72
|
+
expect(result.success).toBe(true);
|
|
73
|
+
// The REST connector handled the dispatch: one fetch with auth + body.
|
|
74
|
+
expect(calls).toHaveLength(1);
|
|
75
|
+
expect(calls[0].url).toBe('https://api.example.com/items');
|
|
76
|
+
expect(calls[0].init.method).toBe('POST');
|
|
77
|
+
expect((calls[0].init.headers as Record<string, string>)['Authorization']).toBe('Bearer secret-token');
|
|
78
|
+
// The action output propagated back into the flow.
|
|
79
|
+
expect(result.output).toEqual({ 'call.body': { id: 'created-1' } });
|
|
80
|
+
|
|
81
|
+
await kernel.shutdown();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { Plugin, PluginContext } from '@objectstack/core';
|
|
4
|
+
import type { Connector } from '@objectstack/spec/integration';
|
|
5
|
+
import { createRestConnector, type RestConnectorOptions } from './rest-connector.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 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`.
|
|
11
|
+
*/
|
|
12
|
+
export interface ConnectorRegistrySurface {
|
|
13
|
+
registerConnector(
|
|
14
|
+
def: Connector,
|
|
15
|
+
handlers: Record<
|
|
16
|
+
string,
|
|
17
|
+
(input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>
|
|
18
|
+
>,
|
|
19
|
+
): void;
|
|
20
|
+
unregisterConnector(name: string): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConnectorRestPluginOptions extends RestConnectorOptions {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
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.
|
|
30
|
+
*
|
|
31
|
+
* If no automation engine is present the plugin logs and skips — the connector
|
|
32
|
+
* has nowhere to register, which is not an error.
|
|
33
|
+
*/
|
|
34
|
+
export class ConnectorRestPlugin implements Plugin {
|
|
35
|
+
name = 'com.objectstack.connector.rest';
|
|
36
|
+
version = '1.0.0';
|
|
37
|
+
type = 'standard' as const;
|
|
38
|
+
// Ensure the automation engine (and its connector registry) is started first.
|
|
39
|
+
dependencies = ['com.objectstack.service-automation'];
|
|
40
|
+
|
|
41
|
+
private readonly options: ConnectorRestPluginOptions;
|
|
42
|
+
private connectorName?: string;
|
|
43
|
+
private automation?: ConnectorRegistrySurface;
|
|
44
|
+
|
|
45
|
+
constructor(options: ConnectorRestPluginOptions) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
}
|
|
48
|
+
|
|
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.
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
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
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!automation || typeof automation.registerConnector !== 'function') {
|
|
63
|
+
ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { def, handlers } = createRestConnector(this.options);
|
|
68
|
+
automation.registerConnector(def, handlers);
|
|
69
|
+
this.automation = automation;
|
|
70
|
+
this.connectorName = def.name;
|
|
71
|
+
ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async stop(_ctx: PluginContext): Promise<void> {
|
|
75
|
+
if (this.automation && this.connectorName) {
|
|
76
|
+
try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @objectstack/connector-rest
|
|
5
|
+
*
|
|
6
|
+
* Generic REST connector — the reference *concrete* connector (ADR-0018
|
|
7
|
+
* §Addendum). The baseline automation engine ships the `connector_action`
|
|
8
|
+
* dispatch node + an empty connector registry; this plugin populates the
|
|
9
|
+
* registry with a `rest` connector exposing a `request` action.
|
|
10
|
+
*
|
|
11
|
+
* Static auth only (`none` / `api-key` / `basic` / `bearer`); OAuth2 refresh,
|
|
12
|
+
* credential vaulting, and multi-tenant lifecycle are the enterprise tier.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
createRestConnector,
|
|
17
|
+
type RestConnectorOptions,
|
|
18
|
+
type RestConnectorBundle,
|
|
19
|
+
type RestRequestInput,
|
|
20
|
+
type RestAuth,
|
|
21
|
+
} from './rest-connector.js';
|
|
22
|
+
export {
|
|
23
|
+
ConnectorRestPlugin,
|
|
24
|
+
type ConnectorRestPluginOptions,
|
|
25
|
+
type ConnectorRegistrySurface,
|
|
26
|
+
} from './connector-rest-plugin.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { createRestConnector } from './rest-connector.js';
|
|
5
|
+
|
|
6
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface CapturedCall {
|
|
9
|
+
url: string;
|
|
10
|
+
init: RequestInit;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A fetch stub that records calls and returns a fixed JSON response. */
|
|
14
|
+
function stubFetch(responseBody: unknown = { ok: true }, status = 200) {
|
|
15
|
+
const calls: CapturedCall[] = [];
|
|
16
|
+
const impl = (async (url: string, init: RequestInit) => {
|
|
17
|
+
calls.push({ url, init });
|
|
18
|
+
return {
|
|
19
|
+
status,
|
|
20
|
+
ok: status >= 200 && status < 300,
|
|
21
|
+
headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
|
|
22
|
+
json: async () => responseBody,
|
|
23
|
+
text: async () => JSON.stringify(responseBody),
|
|
24
|
+
};
|
|
25
|
+
}) as unknown as typeof fetch;
|
|
26
|
+
return { impl, calls };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function headersOf(call: CapturedCall): Record<string, string> {
|
|
30
|
+
return (call.init.headers ?? {}) as Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── request action ──────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe('createRestConnector — request action', () => {
|
|
36
|
+
it('builds the URL from base + path + query and returns the parsed body', async () => {
|
|
37
|
+
const { impl, calls } = stubFetch({ id: 1, name: 'Ada' });
|
|
38
|
+
const { def, handlers } = createRestConnector({ baseUrl: 'https://api.example.com/', fetchImpl: impl });
|
|
39
|
+
|
|
40
|
+
expect(def.name).toBe('rest');
|
|
41
|
+
expect(def.actions?.[0].key).toBe('request');
|
|
42
|
+
|
|
43
|
+
const out = await handlers.request({ path: '/users', query: { page: 2, active: true } }, {});
|
|
44
|
+
|
|
45
|
+
expect(calls).toHaveLength(1);
|
|
46
|
+
expect(calls[0].url).toBe('https://api.example.com/users?page=2&active=true');
|
|
47
|
+
expect(calls[0].init.method).toBe('GET');
|
|
48
|
+
expect(out).toEqual({ status: 200, ok: true, body: { id: 1, name: 'Ada' } });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('JSON-encodes the body and sets Content-Type for non-GET', async () => {
|
|
52
|
+
const { impl, calls } = stubFetch();
|
|
53
|
+
const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
|
|
54
|
+
|
|
55
|
+
await handlers.request({ method: 'post', path: 'items', body: { name: 'x' } }, {});
|
|
56
|
+
|
|
57
|
+
expect(calls[0].init.method).toBe('POST');
|
|
58
|
+
expect(calls[0].init.body).toBe('{"name":"x"}');
|
|
59
|
+
expect(headersOf(calls[0])['Content-Type']).toBe('application/json');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not send a body on GET', async () => {
|
|
63
|
+
const { impl, calls } = stubFetch();
|
|
64
|
+
const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
|
|
65
|
+
|
|
66
|
+
await handlers.request({ method: 'GET', path: '/ping', body: { ignored: true } }, {});
|
|
67
|
+
expect(calls[0].init.body).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ─── auth injection ──────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe('createRestConnector — static auth', () => {
|
|
74
|
+
it('injects a bearer token', async () => {
|
|
75
|
+
const { impl, calls } = stubFetch();
|
|
76
|
+
const { handlers } = createRestConnector({
|
|
77
|
+
baseUrl: 'https://api.example.com',
|
|
78
|
+
auth: { type: 'bearer', token: 'tok-123' },
|
|
79
|
+
fetchImpl: impl,
|
|
80
|
+
});
|
|
81
|
+
await handlers.request({ path: '/me' }, {});
|
|
82
|
+
expect(headersOf(calls[0])['Authorization']).toBe('Bearer tok-123');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('injects a basic auth header', async () => {
|
|
86
|
+
const { impl, calls } = stubFetch();
|
|
87
|
+
const { handlers } = createRestConnector({
|
|
88
|
+
baseUrl: 'https://api.example.com',
|
|
89
|
+
auth: { type: 'basic', username: 'user', password: 'pass' },
|
|
90
|
+
fetchImpl: impl,
|
|
91
|
+
});
|
|
92
|
+
await handlers.request({ path: '/me' }, {});
|
|
93
|
+
const expected = `Basic ${Buffer.from('user:pass').toString('base64')}`;
|
|
94
|
+
expect(headersOf(calls[0])['Authorization']).toBe(expected);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('injects an api-key header by default', async () => {
|
|
98
|
+
const { impl, calls } = stubFetch();
|
|
99
|
+
const { handlers } = createRestConnector({
|
|
100
|
+
baseUrl: 'https://api.example.com',
|
|
101
|
+
auth: { type: 'api-key', key: 'k-1', headerName: 'X-Api-Key' },
|
|
102
|
+
fetchImpl: impl,
|
|
103
|
+
});
|
|
104
|
+
await handlers.request({ path: '/me' }, {});
|
|
105
|
+
expect(headersOf(calls[0])['X-Api-Key']).toBe('k-1');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('injects an api-key as a query param when paramName is set', async () => {
|
|
109
|
+
const { impl, calls } = stubFetch();
|
|
110
|
+
const { handlers } = createRestConnector({
|
|
111
|
+
baseUrl: 'https://api.example.com',
|
|
112
|
+
auth: { type: 'api-key', key: 'k-1', headerName: 'X-API-Key', paramName: 'api_key' },
|
|
113
|
+
fetchImpl: impl,
|
|
114
|
+
});
|
|
115
|
+
await handlers.request({ path: '/me' }, {});
|
|
116
|
+
expect(calls[0].url).toBe('https://api.example.com/me?api_key=k-1');
|
|
117
|
+
expect(headersOf(calls[0])['X-API-Key']).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('adds no auth for type none', async () => {
|
|
121
|
+
const { impl, calls } = stubFetch();
|
|
122
|
+
const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
|
|
123
|
+
await handlers.request({ path: '/public' }, {});
|
|
124
|
+
expect(headersOf(calls[0])['Authorization']).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('merges defaultHeaders, with per-request headers winning', async () => {
|
|
128
|
+
const { impl, calls } = stubFetch();
|
|
129
|
+
const { handlers } = createRestConnector({
|
|
130
|
+
baseUrl: 'https://api.example.com',
|
|
131
|
+
defaultHeaders: { 'X-Trace': 'on', 'X-Env': 'prod' },
|
|
132
|
+
fetchImpl: impl,
|
|
133
|
+
});
|
|
134
|
+
await handlers.request({ path: '/x', headers: { 'X-Env': 'dev' } }, {});
|
|
135
|
+
const h = headersOf(calls[0]);
|
|
136
|
+
expect(h['X-Trace']).toBe('on');
|
|
137
|
+
expect(h['X-Env']).toBe('dev');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { Connector } from '@objectstack/spec/integration';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic REST connector — the reference *concrete* connector (ADR-0018
|
|
7
|
+
* §Addendum). It produces a {@link Connector} definition plus the handler for
|
|
8
|
+
* its one action, `request`, which the baseline `connector_action` node
|
|
9
|
+
* dispatches to.
|
|
10
|
+
*
|
|
11
|
+
* Open-source scope: **static** auth only (`none` / `api-key` / `basic` /
|
|
12
|
+
* `bearer`), with credentials supplied by the caller. OAuth2 token acquisition
|
|
13
|
+
* and refresh, credential vaulting, and multi-tenant connection lifecycle are
|
|
14
|
+
* the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are
|
|
15
|
+
* deliberately out of scope here.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Auth config understood by the REST connector (the static subset). */
|
|
19
|
+
export type RestAuth = Extract<
|
|
20
|
+
Connector['authentication'],
|
|
21
|
+
{ type: 'none' | 'api-key' | 'basic' | 'bearer' }
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export interface RestConnectorOptions {
|
|
25
|
+
/** Connector machine name (snake_case). Defaults to `rest`. */
|
|
26
|
+
name?: string;
|
|
27
|
+
/** Human-readable label. Defaults to a title derived from `name`. */
|
|
28
|
+
label?: string;
|
|
29
|
+
/** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
/** Static authentication. Defaults to `{ type: 'none' }`. */
|
|
32
|
+
auth?: RestAuth;
|
|
33
|
+
/** Headers merged into every request (request-level headers win). */
|
|
34
|
+
defaultHeaders?: Record<string, string>;
|
|
35
|
+
/** Injected for tests; defaults to the global `fetch`. */
|
|
36
|
+
fetchImpl?: typeof fetch;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Input accepted by the `request` action. */
|
|
40
|
+
export interface RestRequestInput {
|
|
41
|
+
method?: string;
|
|
42
|
+
path?: string;
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
45
|
+
body?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A connector definition paired with its action handlers, ready for registerConnector(). */
|
|
49
|
+
export interface RestConnectorBundle {
|
|
50
|
+
def: Connector;
|
|
51
|
+
handlers: Record<
|
|
52
|
+
string,
|
|
53
|
+
(input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>
|
|
54
|
+
>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build the request URL from base + path + query, encoding query params. */
|
|
58
|
+
function buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {
|
|
59
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
60
|
+
const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';
|
|
61
|
+
const url = new URL(base + suffix);
|
|
62
|
+
if (query) {
|
|
63
|
+
for (const [key, value] of Object.entries(query)) {
|
|
64
|
+
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return url.toString();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Apply static auth to the outgoing headers / query. Returns possibly-extended
|
|
72
|
+
* query so an `api-key` configured with `paramName` can ride the query string.
|
|
73
|
+
*/
|
|
74
|
+
function applyAuth(
|
|
75
|
+
auth: RestAuth,
|
|
76
|
+
headers: Record<string, string>,
|
|
77
|
+
query: Record<string, string | number | boolean | null | undefined>,
|
|
78
|
+
): void {
|
|
79
|
+
switch (auth.type) {
|
|
80
|
+
case 'none':
|
|
81
|
+
return;
|
|
82
|
+
case 'bearer':
|
|
83
|
+
headers['Authorization'] = `Bearer ${auth.token}`;
|
|
84
|
+
return;
|
|
85
|
+
case 'basic': {
|
|
86
|
+
const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
|
|
87
|
+
headers['Authorization'] = `Basic ${encoded}`;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case 'api-key':
|
|
91
|
+
if (auth.paramName) query[auth.paramName] = auth.key;
|
|
92
|
+
else headers[auth.headerName ?? 'X-API-Key'] = auth.key;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {
|
|
98
|
+
const name = opts.name ?? 'rest';
|
|
99
|
+
const auth: RestAuth = opts.auth ?? { type: 'none' };
|
|
100
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
101
|
+
|
|
102
|
+
const def: Connector = {
|
|
103
|
+
name,
|
|
104
|
+
label: opts.label ?? 'REST Connector',
|
|
105
|
+
type: 'api',
|
|
106
|
+
description: 'Generic REST/HTTP connector with static authentication.',
|
|
107
|
+
icon: 'globe',
|
|
108
|
+
authentication: auth,
|
|
109
|
+
// Defaulted by ConnectorSchema; set explicitly so the literal satisfies
|
|
110
|
+
// the (post-parse) Connector output type.
|
|
111
|
+
status: 'active',
|
|
112
|
+
enabled: true,
|
|
113
|
+
connectionTimeoutMs: 30000,
|
|
114
|
+
requestTimeoutMs: 30000,
|
|
115
|
+
actions: [
|
|
116
|
+
{
|
|
117
|
+
key: 'request',
|
|
118
|
+
label: 'HTTP Request',
|
|
119
|
+
description: 'Send an HTTP request to the connector\'s base URL with static auth applied.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
method: { type: 'string', description: 'HTTP method (default GET)' },
|
|
124
|
+
path: { type: 'string', description: 'Path appended to the base URL' },
|
|
125
|
+
headers: { type: 'object', description: 'Per-request headers' },
|
|
126
|
+
query: { type: 'object', description: 'Query parameters' },
|
|
127
|
+
body: { description: 'Request body (JSON-encoded for non-GET)' },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
outputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
status: { type: 'number' },
|
|
134
|
+
ok: { type: 'boolean' },
|
|
135
|
+
body: {},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
143
|
+
const req = input as RestRequestInput;
|
|
144
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
145
|
+
const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };
|
|
146
|
+
const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };
|
|
147
|
+
|
|
148
|
+
applyAuth(auth, headers, query);
|
|
149
|
+
|
|
150
|
+
const url = buildUrl(opts.baseUrl, req.path ?? '', query);
|
|
151
|
+
|
|
152
|
+
const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';
|
|
153
|
+
if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {
|
|
154
|
+
headers['Content-Type'] = 'application/json';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const response = await doFetch(url, {
|
|
158
|
+
method,
|
|
159
|
+
headers,
|
|
160
|
+
body: hasBody ? JSON.stringify(req.body) : undefined,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Parse JSON when advertised; fall back to text so non-JSON endpoints
|
|
164
|
+
// don't throw.
|
|
165
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
166
|
+
const parsed = contentType.includes('application/json')
|
|
167
|
+
? await response.json()
|
|
168
|
+
: await response.text();
|
|
169
|
+
|
|
170
|
+
return { status: response.status, ok: response.ok, body: parsed };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { def, handlers: { request } };
|
|
174
|
+
}
|