@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.
@@ -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
+ CLI Building entry: src/index.ts
6
+ CLI Using tsconfig: tsconfig.json
7
+ CLI tsup v8.5.1
8
+ CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
9
+ CLI Target: es2020
10
+ CLI Cleaning output folder
11
+ ESM Build start
12
+ CJS Build start
13
+ ESM dist/index.mjs 4.58 KB
14
+ ESM dist/index.mjs.map 12.89 KB
15
+ ESM ⚡️ Build success in 69ms
16
+ CJS dist/index.js 5.65 KB
17
+ CJS dist/index.js.map 13.84 KB
18
+ CJS ⚡️ Build success in 94ms
19
+ DTS Build start
20
+ DTS ⚡️ Build success in 13989ms
21
+ DTS dist/index.d.mts 3.62 KB
22
+ DTS dist/index.d.ts 3.62 KB
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.
@@ -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 };
@@ -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":"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","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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["dist", "node_modules", "**/*.test.ts"]
10
+ }