Skip to content

Commit a438ad5

Browse files
authored
feat: impl proxy (#45)
1 parent b692c26 commit a438ad5

362 files changed

Lines changed: 18296 additions & 665 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/runtime-api.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,25 @@ under `OOMOL_CONNECT_DATA_DIR/files` and are cleaned up by age.
132132
- `GET /v1/apps/authenticated`
133133
- `POST /v1/proxy/:service`
134134

135-
`POST /v1/proxy/:service` currently returns `proxy_not_supported` until a provider proxy runtime is
136-
implemented.
135+
`POST /v1/proxy/:service` proxies one provider API request when that provider has a registered or
136+
provider-specific local proxy executor. Providers without a proxy executor return `proxy_not_supported`.
137+
138+
Request body:
139+
140+
```json
141+
{
142+
"endpoint": "/provider/path",
143+
"method": "GET",
144+
"query": { "limit": "10" },
145+
"headers": { "accept": "application/json" },
146+
"body": { "name": "example" }
147+
}
148+
```
149+
150+
`endpoint` must be a relative path beginning with `/`; absolute URLs are rejected. The runtime keeps
151+
stored credentials local and lets the provider proxy executor apply provider-specific authentication.
152+
Successful responses use the standard `/v1` success envelope with `data.status`, `data.headers`, and
153+
`data.data`.
137154

138155
## Local Admin Endpoints
139156

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111
"dev": "node scripts/dev-local.ts",
1212
"dev:api": "node scripts/ensure-generated.ts && node src/server/index.ts",
1313
"build": "npm run typecheck",
14+
"build:cloudflare": "npm run generate:catalog && npm run build:web && npm run copy:catalog-assets && wrangler deploy --dry-run --config wrangler.local.jsonc --minify",
1415
"build:web": "npm run build --workspace web",
15-
"dev:cloudflare": "npm run generate:catalog && npm run build:web && node scripts/copy-catalog-assets.ts && wrangler dev --config wrangler.local.jsonc",
16-
"deploy:cloudflare": "npm run generate:catalog && npm run build:web && node scripts/copy-catalog-assets.ts && wrangler deploy --config wrangler.local.jsonc",
16+
"dev:web": "npm run dev --workspace web",
17+
"dev:cloudflare": "npm run generate:catalog && npm run build:web && npm run copy:catalog-assets && wrangler dev --config wrangler.local.jsonc",
18+
"deploy:cloudflare": "npm run generate:catalog && npm run build:web && npm run copy:catalog-assets && wrangler deploy --config wrangler.local.jsonc --minify",
1719
"start": "node scripts/ensure-generated.ts && node src/server/index.ts",
1820
"test": "vitest run",
1921
"typecheck": "npm run generate:registry && node scripts/typecheck.ts src scripts-all examples",
2022
"generate:registry": "node scripts/generate-provider-registry.ts",
2123
"generate:catalog": "npm run generate:registry && node scripts/generate-catalog.ts",
24+
"copy:catalog-assets": "node scripts/copy-catalog-assets.ts",
25+
"search:actions": "node scripts/ensure-generated.ts && node scripts/search-actions.ts",
2226
"runtime:data": "node scripts/runtime-data.ts",
2327
"lint": "oxlint .",
2428
"lint:fix": "oxlint . --fix",

scripts/generate-provider-registry.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ function propertyName(service: string): string {
1616
return /^[A-Za-z_$][\w$]*$/.test(service) ? service : JSON.stringify(service);
1717
}
1818

19-
const lines = [
20-
'import type { CredentialValidators, ProviderExecutors } from "../core/types.ts";',
19+
const registryLines = [
20+
'import type { CredentialValidators, ProviderExecutors, ProviderProxyExecutor } from "../core/types.ts";',
2121
"",
2222
"/** Lazy-loaded provider executor module shape. */",
2323
"export type ExecutorModule = {",
2424
" credentialValidators?: CredentialValidators;",
2525
" executors: ProviderExecutors;",
26+
" proxy?: ProviderProxyExecutor;",
2627
"};",
2728
"",
2829
"/** Generated lazy imports for provider executors. Do not hand-edit. */",
@@ -43,7 +44,7 @@ const lines = [
4344
];
4445

4546
const registryPath = join(providersDir, "registry.generated.ts");
46-
const registryContent = `${lines.join("\n")}\n`;
47+
const registryContent = `${registryLines.join("\n")}\n`;
4748
const existingContent = await readTextFile(registryPath);
4849
if (existingContent !== registryContent) {
4950
await writeFile(registryPath, registryContent);

src/connection-service.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,10 @@ class FakeProviderLoader implements IProviderLoader {
497497
return undefined;
498498
}
499499

500+
async loadProxyExecutor(): Promise<undefined> {
501+
return undefined;
502+
}
503+
500504
async loadCredentialValidators(_service: string): Promise<CredentialValidators | undefined> {
501505
return this.validators;
502506
}

src/core/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,40 @@ export type ActionExecutor<TInput = unknown, TOutput = unknown> = (
402402
context: ExecutionContext,
403403
) => Promise<ExecutionResult<TOutput>>;
404404

405+
export interface ProxyRequestInput {
406+
endpoint: string;
407+
method: string;
408+
query?: Record<string, unknown>;
409+
headers?: Record<string, unknown>;
410+
body?: unknown;
411+
}
412+
413+
export interface ProxyResponse {
414+
status: number;
415+
headers: Record<string, string>;
416+
bodyEncoding?: "base64";
417+
data: unknown;
418+
}
419+
420+
export type ProxyExecutionResult =
421+
| {
422+
ok: true;
423+
response: ProxyResponse;
424+
}
425+
| {
426+
ok: false;
427+
error: {
428+
code: string;
429+
message: string;
430+
details?: unknown;
431+
};
432+
};
433+
434+
export type ProviderProxyExecutor = (
435+
input: ProxyRequestInput,
436+
context: ExecutionContext,
437+
) => Promise<ProxyExecutionResult>;
438+
405439
/**
406440
* Executor map for one provider.
407441
*

src/mcp.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ class EchoProviderLoader implements IProviderLoader {
159159
return async (input) => ({ ok: true, output: input });
160160
}
161161

162+
async loadProxyExecutor(): Promise<undefined> {
163+
return undefined;
164+
}
165+
162166
async loadCredentialValidators(): Promise<undefined> {
163167
return undefined;
164168
}

src/oauth/oauth-flow-service.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,10 @@ class EmptyProviderLoader implements IProviderLoader {
565565
return undefined;
566566
}
567567

568+
async loadProxyExecutor(): Promise<undefined> {
569+
return undefined;
570+
}
571+
568572
async loadCredentialValidators(_service: string): Promise<CredentialValidators | undefined> {
569573
return undefined;
570574
}

src/providers/adobe_commerce/executors.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { CredentialValidators, ExecutionContext, ProviderExecutors } from "../../core/types.ts";
1+
import type {
2+
CredentialValidators,
3+
ExecutionContext,
4+
ProviderExecutors,
5+
ProviderProxyExecutor,
6+
} from "../../core/types.ts";
27
import type { AdobeCommerceActionName } from "./actions.ts";
38

49
import {
@@ -11,6 +16,7 @@ import {
1116
} from "../../core/cast.ts";
1217
import { assertPublicHttpUrl } from "../../core/request.ts";
1318
import {
19+
defineProviderProxy,
1420
defineProviderExecutors,
1521
providerUserAgent,
1622
ProviderRequestError,
@@ -134,6 +140,20 @@ export const executors: ProviderExecutors = defineProviderExecutors<AdobeCommerc
134140
},
135141
});
136142

143+
export const proxy: ProviderProxyExecutor = defineProviderProxy({
144+
service,
145+
baseUrl: async (context) => {
146+
const credential = await requireApiKeyCredential(context, service);
147+
const baseUrl = normalizeAdobeCommerceBaseUrl(credential.metadata.baseUrl ?? credential.values.baseUrl);
148+
const storeCode = normalizeOptionalStoreCode(credential.metadata.storeCode ?? credential.values.storeCode);
149+
const url = new URL(baseUrl);
150+
const segments = [...splitPathSegments(url.pathname), "rest", ...(storeCode ? [storeCode] : []), "V1"];
151+
url.pathname = `/${segments.map((segment) => encodeURIComponent(segment)).join("/")}`;
152+
return url.toString();
153+
},
154+
auth: { type: "api_key_authorization", prefix: "Bearer " },
155+
});
156+
137157
export const credentialValidators: CredentialValidators = {
138158
async apiKey(input, { fetcher, signal }) {
139159
const baseUrl = normalizeAdobeCommerceBaseUrl(input.values.baseUrl);

src/providers/adyen/executors.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import type { CredentialValidators, ExecutionContext, ProviderExecutors } from "../../core/types.ts";
1+
import type {
2+
CredentialValidators,
3+
ExecutionContext,
4+
ProviderExecutors,
5+
ProviderProxyExecutor,
6+
} from "../../core/types.ts";
27
import type { AdyenActionName } from "./actions.ts";
38

49
import { compactObject, nullableString, optionalInteger, optionalRecord, optionalString } from "../../core/cast.ts";
510
import {
11+
defineProviderProxy,
612
defineProviderExecutors,
713
providerUserAgent,
814
ProviderRequestError,
@@ -69,6 +75,15 @@ export const executors: ProviderExecutors = defineProviderExecutors<AdyenActionC
6975
},
7076
});
7177

78+
export const proxy: ProviderProxyExecutor = defineProviderProxy({
79+
service,
80+
baseUrl: async (context) => {
81+
const credential = await requireApiKeyCredential(context, service);
82+
return buildAdyenApiBaseUrl(readStoredEnvironment(credential.values, credential.metadata));
83+
},
84+
auth: { type: "api_key_header", name: "x-api-key" },
85+
});
86+
7287
export const credentialValidators: CredentialValidators = {
7388
async apiKey(input, { fetcher, signal }) {
7489
const environment = normalizeAdyenEnvironment(input.values.environment);

src/providers/agora/executors.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
1-
import type { CredentialValidators, ExecutionContext, ProviderExecutors } from "../../core/types.ts";
1+
import type {
2+
CredentialValidators,
3+
ExecutionContext,
4+
ProviderExecutors,
5+
ProviderProxyExecutor,
6+
ProxyExecutionResult,
7+
} from "../../core/types.ts";
28

3-
import { defineProviderExecutors, requireApiKeyCredential } from "../provider-runtime.ts";
4-
import { agoraActionHandlers, readAgoraCustomerId, validateAgoraCredential } from "./runtime.ts";
9+
import { Buffer } from "node:buffer";
10+
import {
11+
createProviderProxyUrl,
12+
defineProviderExecutors,
13+
normalizeProviderProxyHeaders,
14+
providerUserAgent,
15+
ProviderRequestError,
16+
readProviderProxyErrorMessage,
17+
readProviderProxyResponse,
18+
requireApiKeyCredential,
19+
toProviderProxyError,
20+
} from "../provider-runtime.ts";
21+
import { agoraActionHandlers, agoraApiBaseUrl, readAgoraCustomerId, validateAgoraCredential } from "./runtime.ts";
522

623
const service = "agora";
724

@@ -19,6 +36,35 @@ export const executors: ProviderExecutors = defineProviderExecutors({
1936
},
2037
});
2138

39+
export const proxy: ProviderProxyExecutor = async (input, context): Promise<ProxyExecutionResult> => {
40+
try {
41+
const credential = await requireApiKeyCredential(context, service);
42+
const customerId = readAgoraCustomerId(credential.values);
43+
const url = createProviderProxyUrl(agoraApiBaseUrl, input.endpoint, input.query);
44+
const headers = normalizeProviderProxyHeaders(input.headers);
45+
headers.set("authorization", `Basic ${Buffer.from(`${customerId}:${credential.apiKey}`).toString("base64")}`);
46+
headers.set("user-agent", providerUserAgent);
47+
if (input.body !== undefined && !headers.has("content-type") && typeof input.body !== "string") {
48+
headers.set("content-type", "application/json");
49+
}
50+
51+
const response = await fetch(url, {
52+
method: input.method,
53+
headers,
54+
body:
55+
input.body === undefined ? undefined : typeof input.body === "string" ? input.body : JSON.stringify(input.body),
56+
signal: context.signal,
57+
});
58+
if (!response.ok) {
59+
const text = await readProviderProxyErrorMessage(response, "");
60+
throw new ProviderRequestError(response.status, text || `provider request failed with HTTP ${response.status}`);
61+
}
62+
return { ok: true, response: await readProviderProxyResponse(response) };
63+
} catch (error) {
64+
return toProviderProxyError(error, "provider request failed");
65+
}
66+
};
67+
2268
export const credentialValidators: CredentialValidators = {
2369
async apiKey(input, { fetcher, signal }) {
2470
return validateAgoraCredential(

0 commit comments

Comments
 (0)