Skip to content

Commit 11e36e8

Browse files
authored
feat: exclude node-only providers from Cloudflare (#88)
1 parent de70dcc commit 11e36e8

24 files changed

Lines changed: 458 additions & 95 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ node_modules/
66
dist/
77
coverage/
88
catalog/
9-
src/providers/registry.generated.ts
9+
src/providers/registry*.generated.ts
1010
.env
1111
.env.*
1212
!.env.example

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@oomol-lab/open-connector",
3-
"version": "1.0.2",
3+
"version": "1.1.0",
44
"private": true,
55
"workspaces": [
66
"web"

scripts/ensure-generated.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@ import { readdir, stat } from "node:fs/promises";
33
import { join } from "node:path";
44

55
const rootDir = process.cwd();
6-
const registryPath = join(process.cwd(), "src/providers/registry.generated.ts");
6+
const registryPaths = [
7+
join(process.cwd(), "src/providers/registry.generated.ts"),
8+
join(process.cwd(), "src/providers/registry.cloudflare.generated.ts"),
9+
];
710
const catalogDir = join(process.cwd(), "catalog/apps");
811
const sourcePaths = [
912
join(rootDir, "src/core"),
1013
join(rootDir, "src/providers"),
1114
join(rootDir, "scripts/generate-catalog.ts"),
1215
join(rootDir, "scripts/generate-provider-registry.ts"),
16+
join(rootDir, "scripts/provider-source.ts"),
1317
];
14-
const generatedPaths = new Set([registryPath]);
18+
const generatedPaths = new Set(registryPaths);
1519

1620
const sourceMtimeMs = await newestMtimeMs(sourcePaths);
1721

18-
if (!(await isFreshFile(registryPath, sourceMtimeMs))) {
22+
const registriesFresh = await Promise.all(registryPaths.map((path) => isFreshFile(path, sourceMtimeMs)));
23+
if (registriesFresh.some((fresh) => !fresh)) {
1924
runNodeScript("scripts/generate-provider-registry.ts");
2025
}
2126

scripts/generate-provider-registry.ts

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,61 @@
1+
import type { ProviderSource } from "./provider-source.ts";
2+
13
import { readFile, writeFile } from "node:fs/promises";
24
import { join } from "node:path";
35
import { loadProviderSources } from "./provider-source.ts";
46

57
const providersDir = join(process.cwd(), "src/providers");
68
const providerSources = await loadProviderSources();
7-
const services = providerSources.map((source) => source.service);
8-
const executableActionIds = new Map<string, string[]>(
9-
providerSources.map((source) => [
10-
source.service,
11-
source.definition.actions.map((action) => action.id).sort((a, b) => a.localeCompare(b)),
12-
]),
13-
);
9+
10+
await Promise.all([
11+
writeRegistry("registry.generated.ts", providerSources),
12+
writeRegistry(
13+
"registry.cloudflare.generated.ts",
14+
providerSources.filter((source) => !source.nodeOnly),
15+
),
16+
]);
1417

1518
function propertyName(service: string): string {
1619
return /^[A-Za-z_$][\w$]*$/.test(service) ? service : JSON.stringify(service);
1720
}
1821

19-
const registryLines = [
20-
'import type { CredentialValidators, ProviderExecutors, ProviderProxyExecutor } from "../core/types.ts";',
21-
"",
22-
"/** Lazy-loaded provider executor module shape. */",
23-
"export type ExecutorModule = {",
24-
" credentialValidators?: CredentialValidators;",
25-
" executors: ProviderExecutors;",
26-
" proxy?: ProviderProxyExecutor;",
27-
"};",
28-
"",
29-
"/** Generated lazy imports for provider executors. Do not hand-edit. */",
30-
"export const executorModules: Record<string, () => Promise<ExecutorModule>> = {",
31-
...services.map(
32-
(service) => ` ${propertyName(service)}: (): Promise<ExecutorModule> => import("./${service}/executors.ts"),`,
33-
),
34-
"};",
35-
"",
36-
"/** Generated local executable action ids by provider. Do not hand-edit. */",
37-
"export const executableActionIds: Record<string, string[]> = {",
38-
...services.flatMap((service) => [
39-
` ${propertyName(service)}: [`,
40-
...(executableActionIds.get(service) ?? []).map((actionId) => ` ${JSON.stringify(actionId)},`),
41-
" ],",
42-
]),
43-
"};",
44-
];
22+
async function writeRegistry(filename: string, sources: ProviderSource[]): Promise<void> {
23+
const services = sources.map((source) => source.service);
24+
const executableActionIds = new Map<string, string[]>(
25+
sources.map((source) => [
26+
source.service,
27+
source.definition.actions.map((action) => action.id).sort((a, b) => a.localeCompare(b)),
28+
]),
29+
);
30+
const lines = [
31+
'import type { ExecutorModule } from "./provider-loader.ts";',
32+
"",
33+
"/** Generated lazy imports for provider executors. Do not hand-edit. */",
34+
"export const executorModules: Record<string, () => Promise<ExecutorModule>> = {",
35+
...services.map(
36+
(service) => ` ${propertyName(service)}: (): Promise<ExecutorModule> => import("./${service}/executors.ts"),`,
37+
),
38+
"};",
39+
"",
40+
"/** Generated local executable action ids by provider. Do not hand-edit. */",
41+
"export const executableActionIds: Record<string, string[]> = {",
42+
...services.flatMap((service) => [
43+
` ${propertyName(service)}: [`,
44+
...(executableActionIds.get(service) ?? []).map((actionId) => ` ${JSON.stringify(actionId)},`),
45+
" ],",
46+
]),
47+
"};",
48+
];
4549

46-
const registryPath = join(providersDir, "registry.generated.ts");
47-
const registryContent = `${registryLines.join("\n")}\n`;
48-
const existingContent = await readTextFile(registryPath);
49-
if (existingContent !== registryContent) {
50-
await writeFile(registryPath, registryContent);
51-
console.log(`Generated provider registry for ${services.length} providers.`);
52-
} else {
53-
console.log(`Provider registry is up to date for ${services.length} providers.`);
50+
const path = join(providersDir, filename);
51+
const content = `${lines.join("\n")}\n`;
52+
const existingContent = await readTextFile(path);
53+
if (existingContent !== content) {
54+
await writeFile(path, content);
55+
console.log(`Generated ${filename} for ${services.length} providers.`);
56+
} else {
57+
console.log(`${filename} is up to date for ${services.length} providers.`);
58+
}
5459
}
5560

5661
async function readTextFile(path: string): Promise<string | undefined> {

scripts/provider-source.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { assertProviderId } from "../src/core/provider-id.ts";
66

77
export interface ProviderSource {
88
definition: ProviderDefinition;
9+
nodeOnly: boolean;
910
service: string;
1011
}
1112

@@ -23,13 +24,15 @@ export async function loadProviderSources(): Promise<ProviderSource[]> {
2324
assertProviderDefinitionService(service, module.provider.service);
2425
return {
2526
definition: module.provider,
27+
nodeOnly: module.nodeOnly === true,
2628
service,
2729
};
2830
}),
2931
);
3032
}
3133

3234
interface ProviderDefinitionModule {
35+
nodeOnly?: boolean;
3336
provider: ProviderDefinition;
3437
}
3538

src/connection-service.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ const customCredentialProvider: ProviderDefinition = {
7070
actions: [],
7171
};
7272

73+
const catalogOnlyProvider: ProviderDefinition = {
74+
...customCredentialProvider,
75+
service: "catalog_only",
76+
displayName: "Catalog Only",
77+
actions: [
78+
{
79+
id: "catalog_only.query",
80+
service: "catalog_only",
81+
name: "query",
82+
description: "Query the catalog-only provider.",
83+
requiredScopes: [],
84+
providerPermissions: [],
85+
inputSchema: {},
86+
outputSchema: {},
87+
},
88+
],
89+
};
90+
7391
const oauthProvider: ProviderDefinition = {
7492
service: "example",
7593
displayName: "Example",
@@ -113,6 +131,24 @@ afterEach(() => {
113131
});
114132

115133
describe("ConnectionService", () => {
134+
it("rejects connections for providers unavailable in the current runtime", async () => {
135+
const service = createService([catalogOnlyProvider]);
136+
137+
await expect(
138+
service.connectWithCustomCredential("catalog_only", {
139+
values: {
140+
host: "localhost",
141+
password: "secret",
142+
},
143+
}),
144+
).rejects.toMatchObject({
145+
code: "provider_unavailable",
146+
message: "Catalog Only is not available in this runtime.",
147+
});
148+
149+
await expect(service.listConnections()).resolves.toEqual([]);
150+
});
151+
116152
it("exposes no_auth providers as virtual connections", async () => {
117153
const service = createService([hackernewsProvider]);
118154

src/connection-service.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CatalogStore } from "./catalog-store.ts";
1+
import type { CatalogStore, RuntimeProviderDefinition } from "./catalog-store.ts";
22
import type {
33
ApiKeyAuthDefinition,
44
AuthType,
@@ -207,7 +207,7 @@ export class ConnectionService {
207207
}
208208

209209
async connectWithoutAuth(service: string, input: ConnectWithoutAuthInput = {}): Promise<ConnectionSummary> {
210-
const provider = this.getProvider(service);
210+
const provider = this.getAvailableProvider(service);
211211
if (!this.supportsAuth(provider, "no_auth")) {
212212
throw new ConnectionError("unsupported_auth_type", `${service} does not support no_auth.`);
213213
}
@@ -216,7 +216,7 @@ export class ConnectionService {
216216
}
217217

218218
async connectWithApiKey(service: string, input: ConnectWithCredentialInput): Promise<ConnectionSummary> {
219-
const provider = this.getProvider(service);
219+
const provider = this.getAvailableProvider(service);
220220
if (!this.supportsAuth(provider, "api_key")) {
221221
throw new ConnectionError("unsupported_auth_type", `${service} does not support api_key.`);
222222
}
@@ -248,7 +248,7 @@ export class ConnectionService {
248248
}
249249

250250
async connectWithCustomCredential(service: string, input: ConnectWithCredentialInput): Promise<ConnectionSummary> {
251-
const provider = this.getProvider(service);
251+
const provider = this.getAvailableProvider(service);
252252
if (!this.supportsAuth(provider, "custom_credential")) {
253253
throw new ConnectionError("unsupported_auth_type", `${service} does not support custom_credential.`);
254254
}
@@ -281,7 +281,7 @@ export class ConnectionService {
281281
credential: Extract<ResolvedCredential, { authType: "oauth2" }>,
282282
connectionNameInput?: string,
283283
): Promise<ConnectionSummary> {
284-
const provider = this.getProvider(service);
284+
const provider = this.getAvailableProvider(service);
285285
if (!this.supportsAuth(provider, "oauth2")) {
286286
throw new ConnectionError("unsupported_auth_type", `${service} does not support oauth2.`);
287287
}
@@ -357,7 +357,12 @@ export class ConnectionService {
357357
};
358358
}
359359

360-
private getProvider(service: string): ProviderDefinition {
360+
/** Rejects provider setup when none of its catalog actions can execute in this runtime. */
361+
assertProviderAvailable(service: string): void {
362+
this.getAvailableProvider(service);
363+
}
364+
365+
private getProvider(service: string): RuntimeProviderDefinition {
361366
const provider = this.catalog.providers.find((provider) => provider.service === service);
362367
if (!provider) {
363368
throw new ConnectionError("unknown_service", `Unknown service: ${service}.`);
@@ -366,6 +371,15 @@ export class ConnectionService {
366371
return provider;
367372
}
368373

374+
private getAvailableProvider(service: string): RuntimeProviderDefinition {
375+
const provider = this.getProvider(service);
376+
if (provider.actions.length > 0 && provider.execution.locallyExecutableActionCount === 0) {
377+
throw new ConnectionError("provider_unavailable", `${provider.displayName} is not available in this runtime.`);
378+
}
379+
380+
return provider;
381+
}
382+
369383
private supportsAuth(provider: ProviderDefinition, authType: AuthType): boolean {
370384
return provider.authTypes.includes(authType);
371385
}

src/oauth/oauth-flow-service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export class OAuthFlowService {
6464

6565
async startAuthorization(input: OAuthAuthorizationStartInput): Promise<OAuthAuthorizationStart> {
6666
const { service, connectionName } = input;
67+
this.connections.assertProviderAvailable(service);
6768
const auth = this.clientConfigs.getOAuthDefinition(service);
6869
const config = await this.clientConfigs.getConfig(service);
6970
if (!config) {

src/providers/netease_mail/definition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { ProviderDefinition } from "../../core/types.ts";
22

33
import { neteaseMailActions } from "./actions.ts";
44

5+
export const nodeOnly = true;
6+
57
export const provider: ProviderDefinition = {
68
service: "netease_mail",
79
displayName: "NetEase Mail",

0 commit comments

Comments
 (0)