diff --git a/apps/dashboard/src/components/provider-icons.tsx b/apps/dashboard/src/components/provider-icons.tsx index 18568984..2027f62f 100644 --- a/apps/dashboard/src/components/provider-icons.tsx +++ b/apps/dashboard/src/components/provider-icons.tsx @@ -7,6 +7,7 @@ import type { IconSvgProps, SimpleIconData } from "@onequery/ui/icons/svg-icon"; import { IconDatabase, IconHelpCircle, + IconKey, IconNotebook, IconTerminal2, } from "@tabler/icons-react"; @@ -216,6 +217,12 @@ function E2BIcon(props: ProviderIconProps) { ); } +function OnePasswordIcon(props: ProviderIconProps) { + return ( + + ); +} + function MicrosoftClarityIcon({ size = 24, ...props }: ProviderIconProps) { return ( `. The s | Databases | `postgres`, `supabase`, `mysql`, `mongodb` | | Warehouses | `snowflake`, `bigquery`, `aws_athena_connector`, `motherduck`, `cloudflare_d1` | | Observability | `sentry`, `laminar`, `cloudflare_workers_observability` | -| Developer workflow | `github`, `linear`, `jira`, `confluence`, `vercel`, `discord` | +| Developer workflow | `github`, `linear`, `jira`, `confluence`, `vercel`, `discord`, `onepassword` | | Product analytics | `ga`, `amplitude`, `mixpanel`, `posthog`, `microsoft_clarity`, `cloudflare_web_analytics` | | Marketing | `google_search_console`, `amazon_ads`, `linkedin_ads`, `tiktok_marketing`, `sendgrid` | | Productivity | `airtable`, `cal`, `granola` | diff --git a/apps/landing/src/shared/icons/brands.tsx b/apps/landing/src/shared/icons/brands.tsx index 7491d25b..046cf556 100644 --- a/apps/landing/src/shared/icons/brands.tsx +++ b/apps/landing/src/shared/icons/brands.tsx @@ -143,6 +143,33 @@ function E2BIcon({ size, ...props }: IconSvgProps) { ); } +function OnePasswordIcon({ size, ...props }: IconSvgProps) { + return ( + + + + + ); +} + function MicrosoftClarityIcon({ size, ...props }: IconSvgProps) { return ( credentialSchemaMap > matches supported provider "mongodb", "motherduck", "mysql", + "onepassword", "postgres", "posthog", "sendgrid", diff --git a/packages/db/src/connection-guide.ts b/packages/db/src/connection-guide.ts index c4102d23..d8c6c1f8 100644 --- a/packages/db/src/connection-guide.ts +++ b/packages/db/src/connection-guide.ts @@ -522,6 +522,25 @@ export const SOURCE_CONNECT_PROVIDER_GUIDES: SourceConnectProviderGuide[] = [ }, }, }, + { + provider: "onepassword", + summary: + "Connect 1Password through a self-hosted Connect Server API endpoint.", + steps: [ + "Deploy or choose the 1Password Connect Server that can read the target vaults.", + "Create a Connect access token scoped to the vaults and items OneQuery should inspect.", + "Set `credentials.apiBaseUrl` to the Connect Server origin, for example `https://connect.example.com`.", + "Source API calls are read-only; use selectors such as `/v1/vaults` and `/v1/vaults/{vaultUUID}/items`.", + ], + exampleInput: { + sourceKey: "onepassword_main", + credentials: { + type: "onepassword", + apiBaseUrl: "https://connect.example.com", + accessToken: "onepassword_connect_token", + }, + }, + }, { provider: "microsoft_clarity", summary: "Connect Microsoft Clarity with a project Data Export API token.", diff --git a/packages/db/src/credentials.test.ts b/packages/db/src/credentials.test.ts index 696dced5..f683e81d 100644 --- a/packages/db/src/credentials.test.ts +++ b/packages/db/src/credentials.test.ts @@ -35,6 +35,7 @@ import { MotherDuckCredentialsSchema, MongoDBCredentialsSchema, MySQLCredentialsSchema, + OnePasswordCredentialsSchema, normalizeEnvVarName, PostgresCredentialsSchema, PostHogCredentialsSchema, @@ -73,6 +74,7 @@ import type { MotherDuckCredentials, MongoDBCredentials, MySQLCredentials, + OnePasswordCredentials, PostgresCredentials, PostHogCredentials, SendGridCredentials, @@ -1737,6 +1739,38 @@ describe("credentials schemas", () => { }); }); + describe("OnePasswordCredentialsSchema", () => { + it("validates 1Password credentials", () => { + const credentials: OnePasswordCredentials = { + accessToken: "onepassword_connect_token", + apiBaseUrl: "https://connect.example.com", + type: "onepassword", + }; + + const result = OnePasswordCredentialsSchema.safeParse(credentials); + expect(result.success).toBe(true); + }); + + it("rejects missing access token", () => { + const result = OnePasswordCredentialsSchema.safeParse({ + apiBaseUrl: "https://connect.example.com", + type: "onepassword", + }); + + expect(result.success).toBe(false); + }); + + it("rejects invalid API base URL", () => { + const result = OnePasswordCredentialsSchema.safeParse({ + accessToken: "onepassword_connect_token", + apiBaseUrl: "not-a-url", + type: "onepassword", + }); + + expect(result.success).toBe(false); + }); + }); + describe("MicrosoftClarityCredentialsSchema", () => { it("validates Microsoft Clarity credentials", () => { const credentials: MicrosoftClarityCredentials = { @@ -1938,6 +1972,12 @@ describe("credentials schemas", () => { expect(credentialSchemaMap.e2b).toBe(E2BCredentialsSchema); }); + it("should map onepassword to OnePasswordCredentialsSchema", () => { + expect(credentialSchemaMap.onepassword).toBe( + OnePasswordCredentialsSchema + ); + }); + it("should map slack to SlackCredentialsSchema", () => { expect(credentialSchemaMap.slack).toBe(SlackCredentialsSchema); }); @@ -2217,6 +2257,17 @@ describe("credentials schemas", () => { expect(result.type).toBe("microsoft_clarity"); }); + it("should validate 1Password credentials", () => { + const credentials = { + accessToken: "onepassword_connect_token", + apiBaseUrl: "https://connect.example.com", + type: "onepassword", + }; + + const result = validateCredentials(credentials); + expect(result.type).toBe("onepassword"); + }); + it("should validate Cloudflare Web Analytics credentials", () => { const credentials = { accountId: "023e105f4ecef8ad9ca31a8372d0c353", diff --git a/packages/db/src/credentials.ts b/packages/db/src/credentials.ts index ba252a79..537ed30a 100644 --- a/packages/db/src/credentials.ts +++ b/packages/db/src/credentials.ts @@ -445,6 +445,19 @@ export type MicrosoftClarityCredentials = z.infer< typeof MicrosoftClarityCredentialsSchema >; +export const OnePasswordCredentialsSchema = z.object({ + accessToken: requiredOpaqueString("Access token is required"), + apiBaseUrl: trimmedUrl( + "API base URL is required", + "API base URL must be a valid URL" + ).transform((value) => value.replace(/\/+$/, "")), + type: z.literal("onepassword"), +}); + +export type OnePasswordCredentials = z.infer< + typeof OnePasswordCredentialsSchema +>; + export const CloudflareD1CredentialsSchema = z.object({ accountId: trimmedString("Account ID is required"), apiBaseUrl: optionalTrimmedUrl("API base URL must be a valid URL"), @@ -537,6 +550,7 @@ export const CredentialsSchema = z.union([ VercelCredentialsSchema, E2BCredentialsSchema, MicrosoftClarityCredentialsSchema, + OnePasswordCredentialsSchema, CloudflareD1CredentialsSchema, CloudflareWorkersObservabilityCredentialsSchema, CloudflareWebAnalyticsCredentialsSchema, @@ -604,6 +618,7 @@ export const credentialSchemaMap = { motherduck: MotherDuckCredentialsSchema, mongodb: MongoDBCredentialsSchema, mysql: MySQLCredentialsSchema, + onepassword: OnePasswordCredentialsSchema, postgres: PostgresCredentialsSchema, posthog: PostHogCredentialsSchema, sendgrid: SendGridCredentialsSchema, diff --git a/packages/db/src/schema/__snapshots__/data-sources.test.ts.snap b/packages/db/src/schema/__snapshots__/data-sources.test.ts.snap index 2f97cfce..7ba420d5 100644 --- a/packages/db/src/schema/__snapshots__/data-sources.test.ts.snap +++ b/packages/db/src/schema/__snapshots__/data-sources.test.ts.snap @@ -33,6 +33,7 @@ exports[`data-sources schema > matches provider type snapshots 1`] = ` "jira", "vercel", "e2b", + "onepassword", "microsoft_clarity", "linear", "cloudflare_workers_observability", @@ -69,6 +70,7 @@ exports[`data-sources schema > matches provider type snapshots 1`] = ` "jira", "vercel", "e2b", + "onepassword", "microsoft_clarity", "linear", "cloudflare_workers_observability", diff --git a/packages/db/src/source-providers.ts b/packages/db/src/source-providers.ts index aedeb700..5feb8fc8 100644 --- a/packages/db/src/source-providers.ts +++ b/packages/db/src/source-providers.ts @@ -26,6 +26,7 @@ import { MotherDuckCredentialsSchema, MongoDBCredentialsSchema, MySQLCredentialsSchema, + OnePasswordCredentialsSchema, PostgresCredentialsSchema, PostHogCredentialsSchema, SendGridCredentialsSchema, @@ -981,6 +982,37 @@ export const SOURCE_PROVIDER_REGISTRY = { }, }, }, + onepassword: { + label: "1Password", + credentialSchema: OnePasswordCredentialsSchema, + credentialType: "onepassword", + connectable: true, + analysisSource: true, + queryInterface: false, + sourceApiInterface: true, + testable: false, + dashboardConnectable: true, + dashboardCredentialForm: "json", + publicCategory: "Developer workflow", + guide: { + summary: + "Connect 1Password through a self-hosted Connect Server API endpoint.", + steps: [ + "Deploy or choose the 1Password Connect Server that can read the target vaults.", + "Create a Connect access token scoped to the vaults and items OneQuery should inspect.", + "Set `credentials.apiBaseUrl` to the Connect Server origin, for example `https://connect.example.com`.", + "Source API calls are read-only; use selectors such as `/v1/vaults` and `/v1/vaults/{vaultUUID}/items`.", + ], + exampleInput: { + sourceKey: "onepassword_main", + credentials: { + type: "onepassword", + apiBaseUrl: "https://connect.example.com", + accessToken: "onepassword_connect_token", + }, + }, + }, + }, microsoft_clarity: { label: "Microsoft Clarity", credentialSchema: MicrosoftClarityCredentialsSchema, diff --git a/packages/server/src/source-api/adapters/onepassword.ts b/packages/server/src/source-api/adapters/onepassword.ts new file mode 100644 index 00000000..6fa9af21 --- /dev/null +++ b/packages/server/src/source-api/adapters/onepassword.ts @@ -0,0 +1,43 @@ +import type { OnePasswordCredentials } from "@onequery/db/server"; + +import { createSimpleRestSourceApiAdapter } from "./simple-rest"; + +const ONEPASSWORD_DESCRIPTOR_VERSION = "onepassword.v1"; + +export const onePasswordSourceApiAdapter = + createSimpleRestSourceApiAdapter({ + allowedMethods: ["GET"], + apiBaseUrl: (credentials) => credentials.apiBaseUrl, + auth: (credentials) => ({ + token: credentials.accessToken, + type: "bearer", + }), + buildExamples: (sourceKey) => [ + { + command: `onequery api --source ${sourceKey} /v1/vaults`, + description: "List vaults visible to the connected Connect token.", + label: "List vaults", + }, + { + command: `onequery api --source ${sourceKey} /v1/vaults//items`, + description: "List items in one vault.", + label: "List vault items", + }, + { + command: `onequery api --source ${sourceKey} /v1/vaults//items/`, + description: "Fetch one item from a vault.", + label: "Get item", + }, + ], + descriptorVersion: ONEPASSWORD_DESCRIPTOR_VERSION, + notes: [ + "1Password requests are sent to the configured Connect Server API base URL with Authorization bearer auth.", + "Only GET requests are supported. Item and vault mutations are intentionally excluded.", + "Use `params` in the field patch for Connect API query parameters such as `filter`.", + ], + operationNotes: [ + "Selectors should be Connect API paths such as `/v1/vaults` or `/v1/vaults/{vaultUUID}/items`.", + ], + provider: "onepassword", + providerLabel: "1Password", + }); diff --git a/packages/server/src/source-api/adapters/simple-rest-providers.test.ts b/packages/server/src/source-api/adapters/simple-rest-providers.test.ts index 33a769cc..635f6bd8 100644 --- a/packages/server/src/source-api/adapters/simple-rest-providers.test.ts +++ b/packages/server/src/source-api/adapters/simple-rest-providers.test.ts @@ -13,6 +13,7 @@ import { granolaSourceApiAdapter } from "./granola"; import { jiraSourceApiAdapter } from "./jira"; import { linkedInAdsSourceApiAdapter } from "./linkedin-ads"; import { microsoftClaritySourceApiAdapter } from "./microsoft-clarity"; +import { onePasswordSourceApiAdapter } from "./onepassword"; import { sendGridSourceApiAdapter } from "./sendgrid"; import { tiktokMarketingSourceApiAdapter } from "./tiktok-marketing"; import { vercelSourceApiAdapter } from "./vercel"; @@ -822,6 +823,98 @@ describe("simple REST source API providers", () => { ).rejects.toThrow("Unsupported HTTP method override: POST"); }); + it("executes 1Password read-only requests with bearer auth", async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ vaults: [] }), { + headers: { + "content-type": "application/json", + }, + status: 200, + }) + ); + globalThis.fetch = fetchMock as unknown as typeof fetch; + + const source: PreparedSourceConnection = { + credentials: { + accessToken: "op_connect_token", + apiBaseUrl: "https://connect.example.com", + type: "onepassword", + }, + displayName: "1Password", + id: "source_16", + provider: "onepassword", + sourceKey: "onepassword-main", + }; + const descriptor = await onePasswordSourceApiAdapter.describe({ + actor, + source, + }); + const operation = descriptor.operations[0]; + expect(operation?.methodPolicy.allowedMethods).toEqual(["GET"]); + + const plan = await onePasswordSourceApiAdapter.normalize({ + actor, + descriptor, + request: { + body: { kind: "none" }, + fieldPatch: { + params: { + filter: 'title eq "Production"', + }, + }, + headers: [], + operation: "fetch_api", + selector: "/v1/vaults/vault_123/items", + }, + source, + }); + + expect(plan.kind).toBe("http_request"); + if (plan.kind !== "http_request") { + throw new Error("expected HTTP request plan"); + } + expect(plan.url).toBe( + "https://connect.example.com/v1/vaults/vault_123/items?filter=title+eq+%22Production%22" + ); + + const response = await onePasswordSourceApiAdapter.execute({ + actor, + prepared: { + ...plan, + bodyKind: "none", + bodyPaths: [], + headerNames: [], + preparedBinding: "binding", + }, + source, + }); + + expect(response.status).toBe(200); + const [calledUrl, calledInit] = fetchMock.mock.calls[0] ?? []; + expect(String(calledUrl)).toBe( + "https://connect.example.com/v1/vaults/vault_123/items?filter=title+eq+%22Production%22" + ); + expect(calledInit?.headers).toMatchObject({ + Authorization: "Bearer op_connect_token", + }); + + await expect( + onePasswordSourceApiAdapter.normalize({ + actor, + descriptor, + request: { + body: { kind: "none" }, + fieldPatch: undefined, + headers: [], + methodOverride: "POST", + operation: "fetch_api", + selector: "/v1/vaults/vault_123/items", + }, + source, + }) + ).rejects.toThrow("Unsupported HTTP method override: POST"); + }); + it("normalizes Microsoft Clarity live insights requests", async () => { const source: PreparedSourceConnection = { credentials: { diff --git a/packages/server/src/source-api/registry.ts b/packages/server/src/source-api/registry.ts index 73d257e2..c1a5ca58 100644 --- a/packages/server/src/source-api/registry.ts +++ b/packages/server/src/source-api/registry.ts @@ -19,6 +19,7 @@ import { linkedInAdsSourceApiAdapter } from "./adapters/linkedin-ads"; import { microsoftClaritySourceApiAdapter } from "./adapters/microsoft-clarity"; import { mixpanelSourceApiAdapter } from "./adapters/mixpanel"; import { mongodbSourceApiAdapter } from "./adapters/mongodb"; +import { onePasswordSourceApiAdapter } from "./adapters/onepassword"; import { postHogSourceApiAdapter } from "./adapters/posthog"; import { sendGridSourceApiAdapter } from "./adapters/sendgrid"; import { sentrySourceApiAdapter } from "./adapters/sentry"; @@ -88,6 +89,7 @@ export const sourceApiRegistry = createSourceApiRegistry([ microsoftClaritySourceApiAdapter, mixpanelSourceApiAdapter, mongodbSourceApiAdapter, + onePasswordSourceApiAdapter, postHogSourceApiAdapter, sendGridSourceApiAdapter, sentrySourceApiAdapter, diff --git a/packages/ui/src/data-sources/connection-guides/index.ts b/packages/ui/src/data-sources/connection-guides/index.ts index 24fc0a3f..54b049e9 100644 --- a/packages/ui/src/data-sources/connection-guides/index.ts +++ b/packages/ui/src/data-sources/connection-guides/index.ts @@ -46,6 +46,7 @@ export const GUIDE_CONTENT: Record< motherduck: motherduckGuideContent, mongodb: mongodbGuideContent, mysql: mysqlGuideContent, + onepassword: { providerLabel: "1Password" }, postgres: postgresGuideContent, posthog: posthogGuideContent, sendgrid: sendGridGuideContent, diff --git a/packages/ui/src/data-sources/connection-guides/types.ts b/packages/ui/src/data-sources/connection-guides/types.ts index 4a0266ff..9019545b 100644 --- a/packages/ui/src/data-sources/connection-guides/types.ts +++ b/packages/ui/src/data-sources/connection-guides/types.ts @@ -14,6 +14,7 @@ export type DataSourceConnectionGuideProvider = | "motherduck" | "mongodb" | "mysql" + | "onepassword" | "postgres" | "posthog" | "sendgrid"