← 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 · TypeScriptimport { 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))
}