@contractspec/integration.runtime 2.10.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/dispatcher.d.ts +37 -0
- package/dist/channel/dispatcher.js +130 -0
- package/dist/channel/dispatcher.test.d.ts +1 -0
- package/dist/channel/github.d.ts +47 -0
- package/dist/channel/github.js +58 -0
- package/dist/channel/github.test.d.ts +1 -0
- package/dist/channel/index.d.ts +14 -0
- package/dist/channel/index.js +1463 -0
- package/dist/channel/memory-store.d.ts +28 -0
- package/dist/channel/memory-store.js +223 -0
- package/dist/channel/policy.d.ts +23 -0
- package/dist/channel/policy.js +119 -0
- package/dist/channel/policy.test.d.ts +1 -0
- package/dist/channel/postgres-queries.d.ts +11 -0
- package/dist/channel/postgres-queries.js +222 -0
- package/dist/channel/postgres-schema.d.ts +1 -0
- package/dist/channel/postgres-schema.js +94 -0
- package/dist/channel/postgres-store.d.ts +21 -0
- package/dist/channel/postgres-store.js +498 -0
- package/dist/channel/postgres-store.test.d.ts +1 -0
- package/dist/channel/replay-fixtures.d.ts +9 -0
- package/dist/channel/replay-fixtures.js +42 -0
- package/dist/channel/replay.test.d.ts +1 -0
- package/dist/channel/service.d.ts +26 -0
- package/dist/channel/service.js +319 -0
- package/dist/channel/service.test.d.ts +1 -0
- package/dist/channel/slack.d.ts +42 -0
- package/dist/channel/slack.js +82 -0
- package/dist/channel/slack.test.d.ts +1 -0
- package/dist/channel/store.d.ts +83 -0
- package/dist/channel/store.js +1 -0
- package/dist/channel/telemetry.d.ts +17 -0
- package/dist/channel/telemetry.js +1 -0
- package/dist/channel/types.d.ts +115 -0
- package/dist/channel/types.js +1 -0
- package/dist/channel/whatsapp-meta.d.ts +55 -0
- package/dist/channel/whatsapp-meta.js +66 -0
- package/dist/channel/whatsapp-meta.test.d.ts +1 -0
- package/dist/channel/whatsapp-twilio.d.ts +20 -0
- package/dist/channel/whatsapp-twilio.js +61 -0
- package/dist/channel/whatsapp-twilio.test.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1621 -1
- package/dist/node/channel/dispatcher.js +129 -0
- package/dist/node/channel/github.js +57 -0
- package/dist/node/channel/index.js +1462 -0
- package/dist/node/channel/memory-store.js +222 -0
- package/dist/node/channel/policy.js +118 -0
- package/dist/node/channel/postgres-queries.js +221 -0
- package/dist/node/channel/postgres-schema.js +93 -0
- package/dist/node/channel/postgres-store.js +497 -0
- package/dist/node/channel/replay-fixtures.js +41 -0
- package/dist/node/channel/service.js +318 -0
- package/dist/node/channel/slack.js +81 -0
- package/dist/node/channel/store.js +0 -0
- package/dist/node/channel/telemetry.js +0 -0
- package/dist/node/channel/types.js +0 -0
- package/dist/node/channel/whatsapp-meta.js +65 -0
- package/dist/node/channel/whatsapp-twilio.js +60 -0
- package/dist/node/index.js +1621 -1
- package/dist/node/transport/auth-resolver.js +51 -0
- package/dist/node/transport/index.js +162 -0
- package/dist/node/transport/transport-factory.js +77 -0
- package/dist/node/transport/version-negotiator.js +36 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.health.test.d.ts +1 -0
- package/dist/transport/auth-resolver.d.ts +20 -0
- package/dist/transport/auth-resolver.js +52 -0
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.js +163 -0
- package/dist/transport/transport-factory.d.ts +31 -0
- package/dist/transport/transport-factory.js +78 -0
- package/dist/transport/version-negotiator.d.ts +14 -0
- package/dist/transport/version-negotiator.js +37 -0
- package/package.json +273 -6
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/transport/auth-resolver.ts
|
|
2
|
+
import { findAuthConfig } from "@contractspec/lib.contracts-integrations/integrations/auth";
|
|
3
|
+
import {
|
|
4
|
+
buildAuthHeaders,
|
|
5
|
+
refreshOAuth2Token,
|
|
6
|
+
isOAuth2TokenExpired
|
|
7
|
+
} from "@contractspec/lib.contracts-integrations/integrations/auth-helpers";
|
|
8
|
+
async function resolveAuth(options) {
|
|
9
|
+
const authConfig = findAuthConfig(options.supportedAuthMethods, options.activeAuthMethod);
|
|
10
|
+
if (!authConfig) {
|
|
11
|
+
return { headers: {}, tokenRefreshed: false };
|
|
12
|
+
}
|
|
13
|
+
if (authConfig.type === "oauth2" && options.oauth2State) {
|
|
14
|
+
if (isOAuth2TokenExpired(options.oauth2State)) {
|
|
15
|
+
const clientId = options.secrets.clientId ?? "";
|
|
16
|
+
const clientSecret = options.secrets.clientSecret ?? "";
|
|
17
|
+
try {
|
|
18
|
+
const newState = await refreshOAuth2Token(authConfig, options.oauth2State, { clientId, clientSecret }, options.fetchFn);
|
|
19
|
+
const mergedSecrets2 = {
|
|
20
|
+
...options.secrets,
|
|
21
|
+
accessToken: newState.accessToken
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets2),
|
|
25
|
+
tokenRefreshed: true,
|
|
26
|
+
updatedOAuth2State: newState
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return {
|
|
30
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
31
|
+
tokenRefreshed: false
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const mergedSecrets = {
|
|
36
|
+
...options.secrets,
|
|
37
|
+
accessToken: options.oauth2State.accessToken
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets),
|
|
41
|
+
tokenRefreshed: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
46
|
+
tokenRefreshed: false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
resolveAuth
|
|
51
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/transport/transport-factory.ts
|
|
2
|
+
import { findTransportConfig } from "@contractspec/lib.contracts-integrations/integrations/transport";
|
|
3
|
+
|
|
4
|
+
class RestTransportClient {
|
|
5
|
+
config;
|
|
6
|
+
authHeaders;
|
|
7
|
+
fetchFn;
|
|
8
|
+
type = "rest";
|
|
9
|
+
constructor(config, authHeaders = {}, fetchFn = globalThis.fetch) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.authHeaders = authHeaders;
|
|
12
|
+
this.fetchFn = fetchFn;
|
|
13
|
+
}
|
|
14
|
+
async request(method, path, options) {
|
|
15
|
+
const url = new URL(path, this.config.baseUrl ?? "https://localhost");
|
|
16
|
+
if (options?.queryParams) {
|
|
17
|
+
for (const [key, value] of Object.entries(options.queryParams)) {
|
|
18
|
+
url.searchParams.set(key, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const headers = {
|
|
22
|
+
...this.config.defaultHeaders,
|
|
23
|
+
...this.authHeaders,
|
|
24
|
+
...options?.headers
|
|
25
|
+
};
|
|
26
|
+
if (options?.body && !headers["Content-Type"]) {
|
|
27
|
+
headers["Content-Type"] = "application/json";
|
|
28
|
+
}
|
|
29
|
+
const response = await this.fetchFn(url.toString(), {
|
|
30
|
+
method,
|
|
31
|
+
headers,
|
|
32
|
+
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
33
|
+
signal: options?.signal
|
|
34
|
+
});
|
|
35
|
+
const responseHeaders = {};
|
|
36
|
+
response.headers.forEach((value, key) => {
|
|
37
|
+
responseHeaders[key] = value;
|
|
38
|
+
});
|
|
39
|
+
const data = await response.json().catch(() => null);
|
|
40
|
+
let rateLimitRemaining;
|
|
41
|
+
let rateLimitReset;
|
|
42
|
+
if (this.config.rateLimitHeaders) {
|
|
43
|
+
const remaining = responseHeaders[this.config.rateLimitHeaders.remaining];
|
|
44
|
+
const reset = responseHeaders[this.config.rateLimitHeaders.reset];
|
|
45
|
+
if (remaining)
|
|
46
|
+
rateLimitRemaining = Number(remaining);
|
|
47
|
+
if (reset)
|
|
48
|
+
rateLimitReset = Number(reset);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
data,
|
|
52
|
+
status: response.status,
|
|
53
|
+
headers: responseHeaders,
|
|
54
|
+
rateLimitRemaining,
|
|
55
|
+
rateLimitReset
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function createTransportClient(transports, targetType, authHeaders = {}, fetchFn) {
|
|
60
|
+
const config = findTransportConfig(transports, targetType);
|
|
61
|
+
if (!config)
|
|
62
|
+
return;
|
|
63
|
+
switch (config.type) {
|
|
64
|
+
case "rest":
|
|
65
|
+
return new RestTransportClient(config, authHeaders, fetchFn);
|
|
66
|
+
case "mcp":
|
|
67
|
+
case "webhook":
|
|
68
|
+
case "sdk":
|
|
69
|
+
return;
|
|
70
|
+
default:
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/transport/auth-resolver.ts
|
|
76
|
+
import { findAuthConfig } from "@contractspec/lib.contracts-integrations/integrations/auth";
|
|
77
|
+
import {
|
|
78
|
+
buildAuthHeaders,
|
|
79
|
+
refreshOAuth2Token,
|
|
80
|
+
isOAuth2TokenExpired
|
|
81
|
+
} from "@contractspec/lib.contracts-integrations/integrations/auth-helpers";
|
|
82
|
+
async function resolveAuth(options) {
|
|
83
|
+
const authConfig = findAuthConfig(options.supportedAuthMethods, options.activeAuthMethod);
|
|
84
|
+
if (!authConfig) {
|
|
85
|
+
return { headers: {}, tokenRefreshed: false };
|
|
86
|
+
}
|
|
87
|
+
if (authConfig.type === "oauth2" && options.oauth2State) {
|
|
88
|
+
if (isOAuth2TokenExpired(options.oauth2State)) {
|
|
89
|
+
const clientId = options.secrets.clientId ?? "";
|
|
90
|
+
const clientSecret = options.secrets.clientSecret ?? "";
|
|
91
|
+
try {
|
|
92
|
+
const newState = await refreshOAuth2Token(authConfig, options.oauth2State, { clientId, clientSecret }, options.fetchFn);
|
|
93
|
+
const mergedSecrets2 = {
|
|
94
|
+
...options.secrets,
|
|
95
|
+
accessToken: newState.accessToken
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets2),
|
|
99
|
+
tokenRefreshed: true,
|
|
100
|
+
updatedOAuth2State: newState
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
105
|
+
tokenRefreshed: false
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const mergedSecrets = {
|
|
110
|
+
...options.secrets,
|
|
111
|
+
accessToken: options.oauth2State.accessToken
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets),
|
|
115
|
+
tokenRefreshed: false
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
120
|
+
tokenRefreshed: false
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/transport/version-negotiator.ts
|
|
125
|
+
import {
|
|
126
|
+
resolveApiVersion,
|
|
127
|
+
isVersionDeprecated
|
|
128
|
+
} from "@contractspec/lib.contracts-integrations/integrations/versioning";
|
|
129
|
+
function negotiateVersion(policy, connectionOverride) {
|
|
130
|
+
if (!policy) {
|
|
131
|
+
return {
|
|
132
|
+
resolvedVersion: undefined,
|
|
133
|
+
deprecated: false,
|
|
134
|
+
versionHeaders: {},
|
|
135
|
+
versionQueryParams: {}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const version = resolveApiVersion(policy, connectionOverride);
|
|
139
|
+
const deprecated = version ? isVersionDeprecated(policy, version) : false;
|
|
140
|
+
const versionHeaders = {};
|
|
141
|
+
const versionQueryParams = {};
|
|
142
|
+
if (version) {
|
|
143
|
+
if (policy.versionHeader) {
|
|
144
|
+
versionHeaders[policy.versionHeader] = version;
|
|
145
|
+
}
|
|
146
|
+
if (policy.versionQueryParam) {
|
|
147
|
+
versionQueryParams[policy.versionQueryParam] = version;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
resolvedVersion: version,
|
|
152
|
+
deprecated,
|
|
153
|
+
versionHeaders,
|
|
154
|
+
versionQueryParams
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
resolveAuth,
|
|
159
|
+
negotiateVersion,
|
|
160
|
+
createTransportClient,
|
|
161
|
+
RestTransportClient
|
|
162
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/transport/transport-factory.ts
|
|
2
|
+
import { findTransportConfig } from "@contractspec/lib.contracts-integrations/integrations/transport";
|
|
3
|
+
|
|
4
|
+
class RestTransportClient {
|
|
5
|
+
config;
|
|
6
|
+
authHeaders;
|
|
7
|
+
fetchFn;
|
|
8
|
+
type = "rest";
|
|
9
|
+
constructor(config, authHeaders = {}, fetchFn = globalThis.fetch) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.authHeaders = authHeaders;
|
|
12
|
+
this.fetchFn = fetchFn;
|
|
13
|
+
}
|
|
14
|
+
async request(method, path, options) {
|
|
15
|
+
const url = new URL(path, this.config.baseUrl ?? "https://localhost");
|
|
16
|
+
if (options?.queryParams) {
|
|
17
|
+
for (const [key, value] of Object.entries(options.queryParams)) {
|
|
18
|
+
url.searchParams.set(key, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const headers = {
|
|
22
|
+
...this.config.defaultHeaders,
|
|
23
|
+
...this.authHeaders,
|
|
24
|
+
...options?.headers
|
|
25
|
+
};
|
|
26
|
+
if (options?.body && !headers["Content-Type"]) {
|
|
27
|
+
headers["Content-Type"] = "application/json";
|
|
28
|
+
}
|
|
29
|
+
const response = await this.fetchFn(url.toString(), {
|
|
30
|
+
method,
|
|
31
|
+
headers,
|
|
32
|
+
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
33
|
+
signal: options?.signal
|
|
34
|
+
});
|
|
35
|
+
const responseHeaders = {};
|
|
36
|
+
response.headers.forEach((value, key) => {
|
|
37
|
+
responseHeaders[key] = value;
|
|
38
|
+
});
|
|
39
|
+
const data = await response.json().catch(() => null);
|
|
40
|
+
let rateLimitRemaining;
|
|
41
|
+
let rateLimitReset;
|
|
42
|
+
if (this.config.rateLimitHeaders) {
|
|
43
|
+
const remaining = responseHeaders[this.config.rateLimitHeaders.remaining];
|
|
44
|
+
const reset = responseHeaders[this.config.rateLimitHeaders.reset];
|
|
45
|
+
if (remaining)
|
|
46
|
+
rateLimitRemaining = Number(remaining);
|
|
47
|
+
if (reset)
|
|
48
|
+
rateLimitReset = Number(reset);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
data,
|
|
52
|
+
status: response.status,
|
|
53
|
+
headers: responseHeaders,
|
|
54
|
+
rateLimitRemaining,
|
|
55
|
+
rateLimitReset
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function createTransportClient(transports, targetType, authHeaders = {}, fetchFn) {
|
|
60
|
+
const config = findTransportConfig(transports, targetType);
|
|
61
|
+
if (!config)
|
|
62
|
+
return;
|
|
63
|
+
switch (config.type) {
|
|
64
|
+
case "rest":
|
|
65
|
+
return new RestTransportClient(config, authHeaders, fetchFn);
|
|
66
|
+
case "mcp":
|
|
67
|
+
case "webhook":
|
|
68
|
+
case "sdk":
|
|
69
|
+
return;
|
|
70
|
+
default:
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
createTransportClient,
|
|
76
|
+
RestTransportClient
|
|
77
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/transport/version-negotiator.ts
|
|
2
|
+
import {
|
|
3
|
+
resolveApiVersion,
|
|
4
|
+
isVersionDeprecated
|
|
5
|
+
} from "@contractspec/lib.contracts-integrations/integrations/versioning";
|
|
6
|
+
function negotiateVersion(policy, connectionOverride) {
|
|
7
|
+
if (!policy) {
|
|
8
|
+
return {
|
|
9
|
+
resolvedVersion: undefined,
|
|
10
|
+
deprecated: false,
|
|
11
|
+
versionHeaders: {},
|
|
12
|
+
versionQueryParams: {}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const version = resolveApiVersion(policy, connectionOverride);
|
|
16
|
+
const deprecated = version ? isVersionDeprecated(policy, version) : false;
|
|
17
|
+
const versionHeaders = {};
|
|
18
|
+
const versionQueryParams = {};
|
|
19
|
+
if (version) {
|
|
20
|
+
if (policy.versionHeader) {
|
|
21
|
+
versionHeaders[policy.versionHeader] = version;
|
|
22
|
+
}
|
|
23
|
+
if (policy.versionQueryParam) {
|
|
24
|
+
versionQueryParams[policy.versionQueryParam] = version;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
resolvedVersion: version,
|
|
29
|
+
deprecated,
|
|
30
|
+
versionHeaders,
|
|
31
|
+
versionQueryParams
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
negotiateVersion
|
|
36
|
+
};
|
package/dist/runtime.d.ts
CHANGED
|
@@ -101,3 +101,19 @@ export declare function resolveHealthStrategyOrder(options?: HealthRuntimeStrate
|
|
|
101
101
|
export declare function isUnofficialHealthProviderAllowed(providerKey: string, options?: HealthRuntimeStrategyOptions): boolean;
|
|
102
102
|
export declare function ensureConnectionReady(integration: ResolvedIntegration): void;
|
|
103
103
|
export declare function connectionStatusLabel(status: ConnectionStatus): string;
|
|
104
|
+
/**
|
|
105
|
+
* Optional Composio fallback configuration.
|
|
106
|
+
* When present, the IntegrationProviderFactory will delegate unsupported
|
|
107
|
+
* integration keys to Composio's 850+ toolkit catalog.
|
|
108
|
+
*/
|
|
109
|
+
export interface ComposioRuntimeConfig {
|
|
110
|
+
apiKey: string;
|
|
111
|
+
baseUrl?: string;
|
|
112
|
+
preferredTransport?: 'mcp' | 'sdk';
|
|
113
|
+
}
|
|
114
|
+
export interface IntegrationRuntimeConfig {
|
|
115
|
+
secretProvider: SecretProvider;
|
|
116
|
+
telemetry?: IntegrationTelemetryEmitter;
|
|
117
|
+
healthStrategy?: HealthRuntimeStrategyOptions;
|
|
118
|
+
composio?: ComposioRuntimeConfig;
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves authentication credentials for an integration connection.
|
|
3
|
+
*/
|
|
4
|
+
import type { IntegrationAuthConfig, IntegrationAuthType, OAuth2TokenState } from '@contractspec/lib.contracts-integrations/integrations/auth';
|
|
5
|
+
export interface AuthResolutionResult {
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
tokenRefreshed: boolean;
|
|
8
|
+
updatedOAuth2State?: OAuth2TokenState;
|
|
9
|
+
}
|
|
10
|
+
export interface AuthResolverOptions {
|
|
11
|
+
supportedAuthMethods: IntegrationAuthConfig[];
|
|
12
|
+
activeAuthMethod: IntegrationAuthType;
|
|
13
|
+
secrets: Record<string, string>;
|
|
14
|
+
oauth2State?: OAuth2TokenState;
|
|
15
|
+
fetchFn?: typeof globalThis.fetch;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve auth headers, refreshing OAuth2 tokens if needed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveAuth(options: AuthResolverOptions): Promise<AuthResolutionResult>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/transport/auth-resolver.ts
|
|
3
|
+
import { findAuthConfig } from "@contractspec/lib.contracts-integrations/integrations/auth";
|
|
4
|
+
import {
|
|
5
|
+
buildAuthHeaders,
|
|
6
|
+
refreshOAuth2Token,
|
|
7
|
+
isOAuth2TokenExpired
|
|
8
|
+
} from "@contractspec/lib.contracts-integrations/integrations/auth-helpers";
|
|
9
|
+
async function resolveAuth(options) {
|
|
10
|
+
const authConfig = findAuthConfig(options.supportedAuthMethods, options.activeAuthMethod);
|
|
11
|
+
if (!authConfig) {
|
|
12
|
+
return { headers: {}, tokenRefreshed: false };
|
|
13
|
+
}
|
|
14
|
+
if (authConfig.type === "oauth2" && options.oauth2State) {
|
|
15
|
+
if (isOAuth2TokenExpired(options.oauth2State)) {
|
|
16
|
+
const clientId = options.secrets.clientId ?? "";
|
|
17
|
+
const clientSecret = options.secrets.clientSecret ?? "";
|
|
18
|
+
try {
|
|
19
|
+
const newState = await refreshOAuth2Token(authConfig, options.oauth2State, { clientId, clientSecret }, options.fetchFn);
|
|
20
|
+
const mergedSecrets2 = {
|
|
21
|
+
...options.secrets,
|
|
22
|
+
accessToken: newState.accessToken
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets2),
|
|
26
|
+
tokenRefreshed: true,
|
|
27
|
+
updatedOAuth2State: newState
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return {
|
|
31
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
32
|
+
tokenRefreshed: false
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const mergedSecrets = {
|
|
37
|
+
...options.secrets,
|
|
38
|
+
accessToken: options.oauth2State.accessToken
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets),
|
|
42
|
+
tokenRefreshed: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
47
|
+
tokenRefreshed: false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
resolveAuth
|
|
52
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/transport/transport-factory.ts
|
|
3
|
+
import { findTransportConfig } from "@contractspec/lib.contracts-integrations/integrations/transport";
|
|
4
|
+
|
|
5
|
+
class RestTransportClient {
|
|
6
|
+
config;
|
|
7
|
+
authHeaders;
|
|
8
|
+
fetchFn;
|
|
9
|
+
type = "rest";
|
|
10
|
+
constructor(config, authHeaders = {}, fetchFn = globalThis.fetch) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.authHeaders = authHeaders;
|
|
13
|
+
this.fetchFn = fetchFn;
|
|
14
|
+
}
|
|
15
|
+
async request(method, path, options) {
|
|
16
|
+
const url = new URL(path, this.config.baseUrl ?? "https://localhost");
|
|
17
|
+
if (options?.queryParams) {
|
|
18
|
+
for (const [key, value] of Object.entries(options.queryParams)) {
|
|
19
|
+
url.searchParams.set(key, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const headers = {
|
|
23
|
+
...this.config.defaultHeaders,
|
|
24
|
+
...this.authHeaders,
|
|
25
|
+
...options?.headers
|
|
26
|
+
};
|
|
27
|
+
if (options?.body && !headers["Content-Type"]) {
|
|
28
|
+
headers["Content-Type"] = "application/json";
|
|
29
|
+
}
|
|
30
|
+
const response = await this.fetchFn(url.toString(), {
|
|
31
|
+
method,
|
|
32
|
+
headers,
|
|
33
|
+
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
34
|
+
signal: options?.signal
|
|
35
|
+
});
|
|
36
|
+
const responseHeaders = {};
|
|
37
|
+
response.headers.forEach((value, key) => {
|
|
38
|
+
responseHeaders[key] = value;
|
|
39
|
+
});
|
|
40
|
+
const data = await response.json().catch(() => null);
|
|
41
|
+
let rateLimitRemaining;
|
|
42
|
+
let rateLimitReset;
|
|
43
|
+
if (this.config.rateLimitHeaders) {
|
|
44
|
+
const remaining = responseHeaders[this.config.rateLimitHeaders.remaining];
|
|
45
|
+
const reset = responseHeaders[this.config.rateLimitHeaders.reset];
|
|
46
|
+
if (remaining)
|
|
47
|
+
rateLimitRemaining = Number(remaining);
|
|
48
|
+
if (reset)
|
|
49
|
+
rateLimitReset = Number(reset);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
data,
|
|
53
|
+
status: response.status,
|
|
54
|
+
headers: responseHeaders,
|
|
55
|
+
rateLimitRemaining,
|
|
56
|
+
rateLimitReset
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function createTransportClient(transports, targetType, authHeaders = {}, fetchFn) {
|
|
61
|
+
const config = findTransportConfig(transports, targetType);
|
|
62
|
+
if (!config)
|
|
63
|
+
return;
|
|
64
|
+
switch (config.type) {
|
|
65
|
+
case "rest":
|
|
66
|
+
return new RestTransportClient(config, authHeaders, fetchFn);
|
|
67
|
+
case "mcp":
|
|
68
|
+
case "webhook":
|
|
69
|
+
case "sdk":
|
|
70
|
+
return;
|
|
71
|
+
default:
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/transport/auth-resolver.ts
|
|
77
|
+
import { findAuthConfig } from "@contractspec/lib.contracts-integrations/integrations/auth";
|
|
78
|
+
import {
|
|
79
|
+
buildAuthHeaders,
|
|
80
|
+
refreshOAuth2Token,
|
|
81
|
+
isOAuth2TokenExpired
|
|
82
|
+
} from "@contractspec/lib.contracts-integrations/integrations/auth-helpers";
|
|
83
|
+
async function resolveAuth(options) {
|
|
84
|
+
const authConfig = findAuthConfig(options.supportedAuthMethods, options.activeAuthMethod);
|
|
85
|
+
if (!authConfig) {
|
|
86
|
+
return { headers: {}, tokenRefreshed: false };
|
|
87
|
+
}
|
|
88
|
+
if (authConfig.type === "oauth2" && options.oauth2State) {
|
|
89
|
+
if (isOAuth2TokenExpired(options.oauth2State)) {
|
|
90
|
+
const clientId = options.secrets.clientId ?? "";
|
|
91
|
+
const clientSecret = options.secrets.clientSecret ?? "";
|
|
92
|
+
try {
|
|
93
|
+
const newState = await refreshOAuth2Token(authConfig, options.oauth2State, { clientId, clientSecret }, options.fetchFn);
|
|
94
|
+
const mergedSecrets2 = {
|
|
95
|
+
...options.secrets,
|
|
96
|
+
accessToken: newState.accessToken
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets2),
|
|
100
|
+
tokenRefreshed: true,
|
|
101
|
+
updatedOAuth2State: newState
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return {
|
|
105
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
106
|
+
tokenRefreshed: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const mergedSecrets = {
|
|
111
|
+
...options.secrets,
|
|
112
|
+
accessToken: options.oauth2State.accessToken
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
headers: buildAuthHeaders(authConfig, mergedSecrets),
|
|
116
|
+
tokenRefreshed: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
headers: buildAuthHeaders(authConfig, options.secrets),
|
|
121
|
+
tokenRefreshed: false
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/transport/version-negotiator.ts
|
|
126
|
+
import {
|
|
127
|
+
resolveApiVersion,
|
|
128
|
+
isVersionDeprecated
|
|
129
|
+
} from "@contractspec/lib.contracts-integrations/integrations/versioning";
|
|
130
|
+
function negotiateVersion(policy, connectionOverride) {
|
|
131
|
+
if (!policy) {
|
|
132
|
+
return {
|
|
133
|
+
resolvedVersion: undefined,
|
|
134
|
+
deprecated: false,
|
|
135
|
+
versionHeaders: {},
|
|
136
|
+
versionQueryParams: {}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const version = resolveApiVersion(policy, connectionOverride);
|
|
140
|
+
const deprecated = version ? isVersionDeprecated(policy, version) : false;
|
|
141
|
+
const versionHeaders = {};
|
|
142
|
+
const versionQueryParams = {};
|
|
143
|
+
if (version) {
|
|
144
|
+
if (policy.versionHeader) {
|
|
145
|
+
versionHeaders[policy.versionHeader] = version;
|
|
146
|
+
}
|
|
147
|
+
if (policy.versionQueryParam) {
|
|
148
|
+
versionQueryParams[policy.versionQueryParam] = version;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
resolvedVersion: version,
|
|
153
|
+
deprecated,
|
|
154
|
+
versionHeaders,
|
|
155
|
+
versionQueryParams
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
resolveAuth,
|
|
160
|
+
negotiateVersion,
|
|
161
|
+
createTransportClient,
|
|
162
|
+
RestTransportClient
|
|
163
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating transport clients from integration transport configs.
|
|
3
|
+
*/
|
|
4
|
+
import type { IntegrationTransportConfig, IntegrationTransportType, RestTransportConfig } from '@contractspec/lib.contracts-integrations/integrations/transport';
|
|
5
|
+
export interface TransportClient {
|
|
6
|
+
readonly type: IntegrationTransportType;
|
|
7
|
+
request<T>(method: string, path: string, options?: TransportRequestOptions): Promise<TransportResponse<T>>;
|
|
8
|
+
}
|
|
9
|
+
export interface TransportRequestOptions {
|
|
10
|
+
body?: unknown;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
queryParams?: Record<string, string>;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export interface TransportResponse<T> {
|
|
17
|
+
data: T;
|
|
18
|
+
status: number;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
rateLimitRemaining?: number;
|
|
21
|
+
rateLimitReset?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class RestTransportClient implements TransportClient {
|
|
24
|
+
private readonly config;
|
|
25
|
+
private readonly authHeaders;
|
|
26
|
+
private readonly fetchFn;
|
|
27
|
+
readonly type: "rest";
|
|
28
|
+
constructor(config: RestTransportConfig, authHeaders?: Record<string, string>, fetchFn?: typeof globalThis.fetch);
|
|
29
|
+
request<T>(method: string, path: string, options?: TransportRequestOptions): Promise<TransportResponse<T>>;
|
|
30
|
+
}
|
|
31
|
+
export declare function createTransportClient(transports: IntegrationTransportConfig[], targetType: IntegrationTransportType, authHeaders?: Record<string, string>, fetchFn?: typeof globalThis.fetch): TransportClient | undefined;
|