Skip to content
← Public packages

@charlesmatte/granola

Search and read Granola meeting notes, summaries and transcripts from any agent; manage webhooks and legal holds safely.

src/lib/secrets.ts

52 lines · 2.3 KB · TypeScript
Raw
import { kody } from 'kody:runtime'
import { accountFromSecretName, GRANOLA_HOST } from './account.ts'
import { hostApprovalUrl, secretSetupUrl } from './urls.ts'

type SecretMeta = { name?: string; scope?: string; description?: string; allowed_hosts?: Array<string> }

/** User-scoped secret metadata (names, hosts — never values). Null when Kody cannot list them here. */
export async function userSecrets(): Promise<Map<string, SecretMeta> | null> {
	try {
		const listed = (await kody.secretList({ scope: 'user' })) as unknown
		const entries: Array<SecretMeta> =
			listed && typeof listed === 'object' && Array.isArray((listed as { secrets?: unknown }).secrets)
				? (listed as { secrets: Array<SecretMeta> }).secrets
				: Array.isArray(listed)
					? (listed as Array<SecretMeta>)
					: []
		const map = new Map<string, SecretMeta>()
		for (const entry of entries) if (entry?.name && (entry.scope === 'user' || !entry.scope)) map.set(entry.name, entry)
		return map
	} catch {
		return null
	}
}

export type CredentialStatus = {
	secretName: string
	saved: boolean | null
	hostApproved: boolean | null
	ready: boolean | null
	setupUrl?: string
	hostApprovalUrl?: string
}

/** Whether the secret exists and may be sent to public-api.granola.ai (metadata only). */
export function credentialStatus(secretName: string, secrets: Map<string, SecretMeta> | null): CredentialStatus {
	if (!secrets) return { secretName, saved: null, hostApproved: null, ready: null }
	const meta = secrets.get(secretName)
	if (!meta) return { secretName, saved: false, hostApproved: false, ready: false, setupUrl: secretSetupUrl(secretName) }
	const hostApproved = (meta.allowed_hosts ?? []).includes(GRANOLA_HOST)
	return { secretName, saved: true, hostApproved, ready: hostApproved, hostApprovalUrl: hostApproved ? undefined : hostApprovalUrl(secretName) }
}

/** Accounts discovered from user secrets named `granola-<account>`. */
export function discoverAccounts(secrets: Map<string, SecretMeta> | null) {
	if (!secrets) return null
	const out: Array<{ account: string } & CredentialStatus & { description?: string }> = []
	for (const [name, meta] of secrets) {
		const account = accountFromSecretName(name)
		if (account) out.push({ account, ...credentialStatus(name, secrets), description: meta.description })
	}
	return out.sort((x, y) => x.account.localeCompare(y.account))
}