import { createClient, type Client } from "@1password/sdk" import { isPlainObject } from "@zachsents/zippy" import * as z from "zod" import type { ResolvedIntegrationAccount } from "../../automation/integrations" import { ONEPASSWORD_EVENTS_API_ORIGINS, ONEPASSWORD_EVENTS_INTROSPECTION_SCHEMA, type OnePasswordEventsCursor, } from "./schemas" const ONEPASSWORD_SERVICE_ACCOUNT_SECRET_SCHEMA = z.object({ serviceAccountToken: z.string().min(1), }) const ONEPASSWORD_EVENTS_API_SECRET_SCHEMA = z.object({ baseUrl: z.string(), bearerToken: z.string().min(1), }) const ONEPASSWORD_EVENTS_ERROR_SCHEMA = z.looseObject({ Error: z .looseObject({ Message: z.string().optional(), }) .optional(), message: z.string().optional(), }) const ONEPASSWORD_EVENT_DATE_KEYS = new Set([ "issuedAt", "loginTime", "timestamp", ]) export const ONEPASSWORD_SERVICE_ACCOUNT_OPTIONS = { connections: [{ connectionMethodId: "service-account" }], } as const /** * Selects an Events API token with the feature required by one event feed. * * @param requiredScope - Feature the selected account must expose. */ export function onePasswordEventsAccountOptions( requiredScope?: "auditevents" | "itemusages" | "signinattempts", ) { return { connections: [ { connectionMethodId: "events-api-token", ...(requiredScope && { requiredScope }), }, ], } as const } /** * Creates an authenticated official 1Password SDK client. * * @param account - Resolved service-account connection. * @throws {Error} When called with another connection method. */ export function getOnePasswordClient( account: ResolvedIntegrationAccount<"1password">, ): Promise { if (account.connectionMethodId !== "service-account") { throw new Error( `Unsupported 1Password SDK connection method: ${account.connectionMethodId}`, ) } const { serviceAccountToken } = ONEPASSWORD_SERVICE_ACCOUNT_SECRET_SCHEMA.parse(account.secret) return createClient({ auth: serviceAccountToken, integrationName: "Automate.ax", integrationVersion: "v1", }) } /** Structured failure returned by the 1Password Events API. */ export class OnePasswordEventsApiError extends Error { /** HTTP status returned by 1Password. */ readonly status: number /** * Creates an Events API error. * * @param status - HTTP status returned by 1Password. * @param providerMessage - Provider error text, when available. */ constructor(status: number, providerMessage?: string) { super( providerMessage ? `1Password Events API error (${status}): ${providerMessage}` : `1Password Events API request failed with status ${status}.`, ) this.name = "OnePasswordEventsApiError" this.status = status } } /** Official, authenticated 1Password Events API helper. */ export interface OnePasswordEventsApi { /** Retrieves token identity and enabled event features. */ introspect(): Promise< z.output > /** Retrieves and validates one cursor page from an Events API feed. */ list( feed: "auditevents" | "itemusages" | "signinattempts", cursor: OnePasswordEventsCursor, responseSchema: TSchema, ): Promise> } /** * Creates a 1Password Events API client restricted to official origins. * * @param account - Resolved Events API token connection. * @throws {Error} When called with another connection method. */ export function getOnePasswordEventsApi( account: ResolvedIntegrationAccount<"1password">, ): OnePasswordEventsApi { if (account.connectionMethodId !== "events-api-token") { throw new Error( `Unsupported 1Password Events connection method: ${account.connectionMethodId}`, ) } const { baseUrl, bearerToken } = ONEPASSWORD_EVENTS_API_SECRET_SCHEMA.parse( account.secret, ) return createOnePasswordEventsApi(baseUrl, bearerToken) } /** * Creates a direct Events API client for provider validation and action use. * * @param baseUrl - Official 1Password Events API origin. * @param bearerToken - Events Reporting bearer token. */ export function createOnePasswordEventsApi( baseUrl: string, bearerToken: string, ): OnePasswordEventsApi { const origin = parseOnePasswordEventsOrigin(baseUrl) /** * Sends one authenticated Events API request. * * @param path - API path relative to the selected regional origin. * @param init - Optional Fetch request options. * @throws {OnePasswordEventsApiError} When 1Password rejects the request. */ async function request(path: string, init?: RequestInit) { const response = await fetch( new URL(path.replace(/^\//, ""), `${origin}/`), { ...init, headers: { Accept: "application/json", Authorization: `Bearer ${bearerToken}`, ...(init?.body && { "Content-Type": "application/json" }), }, }, ) const text = await response.text() const payload = parseJson(text) if (!response.ok) { const error = ONEPASSWORD_EVENTS_ERROR_SCHEMA.safeParse(payload) throw new OnePasswordEventsApiError( response.status, error.success ? (error.data.Error?.Message ?? error.data.message) : text.trim() || undefined, ) } return payload } return { introspect: async () => ONEPASSWORD_EVENTS_INTROSPECTION_SCHEMA.parse( normalizeOnePasswordEventsValue( await request("api/v2/auth/introspect"), ), ), list: async (feed, cursor, responseSchema) => responseSchema.parse( normalizeOnePasswordEventsValue( await request(`api/v2/${feed}`, { body: JSON.stringify(toOnePasswordEventsCursor(cursor)), method: "POST", }), ), ), } } /** * Validates and canonicalizes an official Events API origin. * * @param value - Submitted Events API URL. * @throws {Error} When the URL isn't an exact official origin. */ export function parseOnePasswordEventsOrigin(value: string) { const parsed = new URL(z.url({ protocol: /^https$/ }).parse(value.trim())) if ( !ONEPASSWORD_EVENTS_API_ORIGINS.some( (origin) => origin === parsed.origin, ) || (parsed.pathname !== "/" && parsed.pathname !== "") || parsed.search || parsed.hash ) { throw new Error(`Unsupported 1Password Events API origin: ${parsed.origin}`) } return parsed.origin } /** * Converts a public cursor input to the provider's wire format. * * @param cursor - Reset or continuation cursor. */ function toOnePasswordEventsCursor(cursor: OnePasswordEventsCursor) { if ("cursor" in cursor) return cursor return { ...(cursor.endTime && { end_time: toIsoString(cursor.endTime) }), ...(cursor.limit && { limit: cursor.limit }), ...(cursor.startTime && { start_time: toIsoString(cursor.startTime) }), } } /** * Converts a date input to an RFC 3339 string. * * @param value - Public date input. */ function toIsoString(value: Date | string) { return value instanceof Date ? value.toISOString() : value } /** * Recursively converts provider field names and date strings to public values. * * @param value - Provider value to normalize. * @param key - Normalized parent field name. */ function normalizeOnePasswordEventsValue( value: unknown, key?: string, ): unknown { if (Array.isArray(value)) { return value.map((item) => normalizeOnePasswordEventsValue(item)) } if (!isPlainObject(value)) { return typeof value === "string" && key && ONEPASSWORD_EVENT_DATE_KEYS.has(key) ? new Date(value) : value } return Object.fromEntries( Object.entries(value).map(([providerKey, item]) => { const publicKey = providerKey.replace(/_([a-z0-9])/g, (_match, letter) => String(letter).toUpperCase(), ) return [publicKey, normalizeOnePasswordEventsValue(item, publicKey)] }), ) } /** * Parses a provider response while preserving non-JSON error text. * * @param value - Raw provider response body. */ function parseJson(value: string): unknown { if (!value) return undefined try { return JSON.parse(value) } catch { return value } }