Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/providers/builder_io/executors.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import type { CredentialValidationResult, CredentialValidators, ProviderExecutors } from "../../core/types.ts";
import type {
CredentialValidationResult,
ProviderProxyExecutor,
CredentialValidators,
ProviderExecutors,
} from "../../core/types.ts";

import { defineApiKeyProviderExecutors } from "../provider-runtime.ts";
import { defineApiKeyProviderExecutors, defineProviderProxy } from "../provider-runtime.ts";
import { builderIoActionHandlers, builderIoWriteApiBaseUrl } from "./runtime.ts";

const service = "builder_io";

export const executors: ProviderExecutors = defineApiKeyProviderExecutors(service, builderIoActionHandlers);

const contentProxy = defineProviderProxy({
service,
baseUrl: "https://cdn.builder.io",
auth: { type: "api_key_query", name: "apiKey" },
});

const writeProxy = defineProviderProxy({
service,
baseUrl: builderIoWriteApiBaseUrl,
auth: { type: "api_key_authorization", prefix: "Bearer " },
});

export const proxy: ProviderProxyExecutor = (input, context) => {
const endpoint = typeof input.endpoint === "string" ? input.endpoint : "";
const path = endpoint.split(/[?#]/u)[0] ?? "";
return path === "/api/v3/content" || path.startsWith("/api/v3/content/")
? contentProxy(input, context)
: writeProxy(input, context);
};

export const credentialValidators: CredentialValidators = {
async apiKey(input): Promise<CredentialValidationResult> {
return {
Expand Down
68 changes: 66 additions & 2 deletions src/providers/googledocs/executors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { CredentialValidators, ExecutionContext, ProviderExecutors } from "../../core/types.ts";
import type {
CredentialValidators,
ExecutionContext,
ProviderExecutors,
ProviderProxyExecutor,
} from "../../core/types.ts";
import type { GoogledocsActionName } from "./actions.ts";

import { Buffer } from "node:buffer";
Expand All @@ -12,10 +17,16 @@ import {
googleRequest as googleRequestShared,
optionalBoolean,
} from "../googledrive/runtime-shared.ts";
import { defineProviderExecutors, ProviderRequestError, requireOAuthCredential } from "../provider-runtime.ts";
import {
defineProviderExecutors,
defineProviderProxy,
ProviderRequestError,
requireOAuthCredential,
} from "../provider-runtime.ts";

const docsApiBaseUrl = "https://docs.googleapis.com/v1";
const driveApiBaseUrl = "https://www.googleapis.com/drive/v3";
const googleApiBaseUrl = "https://www.googleapis.com";
const sheetsApiBaseUrl = "https://sheets.googleapis.com/v4";

type ActionContext = {
Expand All @@ -25,6 +36,30 @@ type ActionContext = {

type ActionHandler = (input: Record<string, unknown>, context: ActionContext) => Promise<unknown>;

const docsProxy = defineProviderProxy({
service: "googledocs",
baseUrl: docsApiBaseUrl,
auth: { type: "oauth_bearer" },
});

const driveProxy = defineProviderProxy({
service: "googledocs",
baseUrl: driveApiBaseUrl,
auth: { type: "oauth_bearer" },
});

const googleApiProxy = defineProviderProxy({
service: "googledocs",
baseUrl: googleApiBaseUrl,
auth: { type: "oauth_bearer" },
});

const sheetsProxy = defineProviderProxy({
service: "googledocs",
baseUrl: sheetsApiBaseUrl,
auth: { type: "oauth_bearer" },
});

type GoogleDocument = Record<string, unknown> & {
documentId?: string;
title?: string;
Expand Down Expand Up @@ -252,6 +287,35 @@ export const executors: ProviderExecutors = defineProviderExecutors<ActionContex
},
});

export const proxy: ProviderProxyExecutor = (input, context) => {
const path = typeof input.endpoint === "string" ? input.endpoint.split(/[?#]/u)[0] : "";
if (path === "/documents" || path.startsWith("/documents/")) {
return docsProxy(input, context);
}
if (path === "/files" || path.startsWith("/files/")) {
return driveProxy(input, context);
}
if (
path === "/drive/v3" ||
path.startsWith("/drive/v3/") ||
path === "/upload/drive/v3" ||
path.startsWith("/upload/drive/v3/")
) {
return googleApiProxy(input, context);
}
if (path === "/spreadsheets" || path.startsWith("/spreadsheets/")) {
return sheetsProxy(input, context);
}
return Promise.resolve({
ok: false,
error: {
code: "invalid_input",
message: "endpoint is not supported for this provider",
details: { status: 400 },
},
});
};

export const credentialValidators: CredentialValidators = {
async oauth2(input, { fetcher }) {
const profile = await googleJsonRequest<{
Expand Down
10 changes: 9 additions & 1 deletion src/providers/huggingface/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const provider: ProviderDefinition = {
service,
displayName: "Hugging Face",
categories: ["AI", "Developer Tools"],
authTypes: ["oauth2"],
authTypes: ["oauth2", "api_key"],
auth: [
{
type: "oauth2",
Expand All @@ -21,6 +21,14 @@ export const provider: ProviderDefinition = {
scopes: huggingfaceOAuthScopes,
tokenEndpointAuthMethod: "client_secret_post",
},
{
type: "api_key",
label: "User Access Token",
placeholder: "hf_...",
description:
"Hugging Face user access token sent as a Bearer token. Create one from your Hugging Face access token settings.",
extraFields: [],
},
],
homepageUrl: "https://huggingface.co",
actions: huggingfaceActions,
Expand Down
51 changes: 51 additions & 0 deletions src/providers/huggingface/executors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { apiKeyCredential } from "../provider-proxy-loader.test-helpers.ts";
import { executors } from "./executors.ts";

afterEach(() => {
vi.unstubAllGlobals();
});

describe("Hugging Face executors", () => {
it("executes current user requests with API key credentials", async () => {
const fetcher = vi.fn(
async (_input: RequestInfo | URL, _init?: RequestInit): Promise<Response> =>
new Response(
JSON.stringify({
id: "user-1",
name: "ada",
fullname: "Ada Lovelace",
email: "ada@example.com",
}),
{ headers: { "content-type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetcher);

const result = await executors["huggingface.get_current_user"]?.(
{},
{
getCredential: async () => apiKeyCredential("hf-token"),
},
);

expect(result).toEqual({
ok: true,
output: {
id: "user-1",
preferredUsername: "ada",
name: "Ada Lovelace",
email: "ada@example.com",
profileUrl: "https://huggingface.co/ada",
},
});
expect(fetcher).toHaveBeenCalledWith(
"https://huggingface.co/api/whoami-v2",
expect.objectContaining({
headers: expect.objectContaining({
authorization: "Bearer hf-token",
}),
}),
);
});
});
96 changes: 74 additions & 22 deletions src/providers/huggingface/executors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { CredentialValidators, ProviderExecutors } from "../../core/types.ts";
import type { OAuthProviderContext } from "../provider-runtime.ts";
import type {
CredentialValidationResult,
CredentialValidators,
ExecutionContext,
ProviderExecutors,
} from "../../core/types.ts";
import type { ProviderFetch } from "../provider-runtime.ts";
import type { HuggingfaceActionName } from "./actions.ts";
import type { HuggingfaceActionContext, HuggingfaceCurrentUser } from "./runtime.shared.ts";

import { compactObject } from "../../core/cast.ts";
import { defineOAuthProviderExecutors } from "../provider-runtime.ts";
import { defineProviderExecutors, ProviderRequestError } from "../provider-runtime.ts";
import {
getHuggingfaceDatasetFirstRows,
getHuggingfaceDatasetInfo,
Expand All @@ -22,7 +28,6 @@ import { getHuggingfaceSpaceInfo, listHuggingfaceRepoFiles, listHuggingfaceSpace

const service = "huggingface";

type HuggingfaceActionContext = OAuthProviderContext;
type HuggingfaceActionHandler = (input: Record<string, unknown>, context: HuggingfaceActionContext) => Promise<unknown>;

export const huggingfaceActionHandlers: Record<HuggingfaceActionName, HuggingfaceActionHandler> = {
Expand Down Expand Up @@ -70,33 +75,80 @@ export const huggingfaceActionHandlers: Record<HuggingfaceActionName, Huggingfac
},
};

export const executors: ProviderExecutors = defineOAuthProviderExecutors(service, huggingfaceActionHandlers);
export const executors: ProviderExecutors = defineProviderExecutors<HuggingfaceActionContext>({
service,
handlers: huggingfaceActionHandlers,
async createContext(context: ExecutionContext, fetcher: ProviderFetch): Promise<HuggingfaceActionContext> {
const credential = await context.getCredential(service);
if (credential?.authType === "oauth2") {
return {
authType: "oauth2",
accessToken: credential.accessToken,
tokenType: credential.tokenType,
fetcher,
signal: context.signal,
transitFiles: context.transitFiles,
};
}
if (credential?.authType === "api_key") {
return {
authType: "api_key",
accessToken: credential.apiKey,
tokenType: "Bearer",
fetcher,
signal: context.signal,
transitFiles: context.transitFiles,
};
}
throw new ProviderRequestError(
401,
"Connect huggingface with OAuth or configure Hugging Face API key credentials first.",
);
},
});

export const credentialValidators: CredentialValidators = {
async oauth2(input, { fetcher, signal }) {
const user = await readHuggingfaceCurrentUser({
authType: "oauth2",
accessToken: input.accessToken,
tokenType: input.tokenType,
fetcher,
signal,
});

return {
profile: {
accountId: user.id,
displayName: user.name ?? user.preferredUsername ?? user.email ?? user.id,
},
metadata: {
currentAccount: compactObject({
sub: user.id,
preferredUsername: user.preferredUsername,
name: user.name,
email: user.email,
avatarUrl: user.avatarUrl,
profileUrl: user.profileUrl,
organizations: user.organizations,
}),
},
};
return credentialValidationResult(user);
},

async apiKey(input, { fetcher, signal }) {
const user = await readHuggingfaceCurrentUser({
authType: "api_key",
accessToken: input.apiKey,
tokenType: "Bearer",
fetcher,
signal,
});

return credentialValidationResult(user);
},
};

function credentialValidationResult(user: HuggingfaceCurrentUser): CredentialValidationResult {
return {
profile: {
accountId: user.id,
displayName: user.name ?? user.preferredUsername ?? user.email ?? user.id,
},
metadata: {
currentAccount: compactObject({
sub: user.id,
preferredUsername: user.preferredUsername,
name: user.name,
email: user.email,
avatarUrl: user.avatarUrl,
profileUrl: user.profileUrl,
organizations: user.organizations,
}),
},
};
}
Loading
Loading