import { plainClone, storageGet } from './plain.ts' import { kody, packageContext, packageStorage } from 'kody:runtime' /** Saved package UUID from package_get_git_remote create. Fallback when packageContext is null. */ export const SAVED_PACKAGE_ID = '5bddf97f-09c7-41eb-8402-a7102432dd2e' export const WEBHOOK_HOST = 'api2.cursor.sh' export const BOT_PREFIX = 'bot:' export const ALIAS_PREFIX = 'alias:' const UUID_LIKE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i export type BotRecord = { id: string name: string aliases: string[] url: string secretName: string channelId?: string createdAt: string updatedAt: string } export type BotSummary = { id: string name: string aliases: string[] secretName: string pendingSecret: boolean needsHostApproval: boolean urlHost: string channelId?: string emailAddress?: string approvalUrl?: string } export type WakeSecretMeta = { name: string scope: string package_id: string | null allowed_hosts: string[] description: string | null updated_at: string | null } export type SecretUseStatus = { exists: boolean usableScope: string | null needsHostApproval: boolean approvalUrl: string | null allowedHosts: string[] } export function normalizeName(name: string): string { return name.trim().toLowerCase() } export function isUuidLike(value: string): boolean { return UUID_LIKE.test(value.trim()) } export function secretNameFor(id: string): string { return `grokBotWake.${id}` } export function botKey(id: string): string { return `${BOT_PREFIX}${id}` } export function aliasKey(normalized: string): string { return `${ALIAS_PREFIX}${normalized}` } export function savedPackageId(): string { // Always grok-bot — secrets/saveUrl must not inherit a dependent's packageContext. return SAVED_PACKAGE_ID } /** True when this code is executing under a different package's isolate. */ export function isForeignPackageIsolate(): boolean { const fromContext = packageContext?.packageId?.trim() return Boolean(fromContext && fromContext !== SAVED_PACKAGE_ID) } export function requireId(id: unknown): string { if (typeof id !== 'string' || id.trim() === '') { throw new Error('id is required and must be a UUID.') } const trimmed = id.trim() if (!isUuidLike(trimmed)) { throw new Error('id must be a UUID-like value (8-4-4-4-12 hex).') } return trimmed } export function requireName(name: unknown): string { if (typeof name !== 'string' || name.trim() === '') { throw new Error('name is required.') } return name.trim() } export function requirePrompt(prompt: unknown): string { if (typeof prompt !== 'string' || prompt.trim() === '') { throw new Error('prompt is required.') } return prompt } export function requireBotQuery(bot: unknown): string { if (typeof bot !== 'string' || bot.trim() === '') { throw new Error('bot is required (id or name).') } return bot.trim() } export function parseWebhookUrl(url: unknown): URL { if (typeof url !== 'string' || url.trim() === '') { throw new Error('url is required and must be https://api2.cursor.sh/…') } let parsed: URL try { parsed = new URL(url.trim()) } catch { throw new Error('url must be a valid https URL on api2.cursor.sh') } if (parsed.protocol !== 'https:') { throw new Error('url must use https.') } if (parsed.hostname !== WEBHOOK_HOST) { throw new Error(`url host must be exactly ${WEBHOOK_HOST}.`) } return parsed } export function buildSaveUrl(input: { secretName: string name: string packageId: string }): string { const url = new URL('https://kody.codes/account/secrets/new') url.searchParams.set('name', input.secretName) url.searchParams.set( 'description', `Grok Bot wake sender key for ${input.name}`, ) url.searchParams.set('allowedHosts', WEBHOOK_HOST) url.searchParams.set('scope', 'package') url.searchParams.set('packageId', input.packageId) return url.toString() } export function buildHostApprovalUrl(input: { scope: string secretName: string }): string { return `https://kody.codes/account/secrets/${encodeURIComponent(input.scope)}/${encodeURIComponent(input.secretName)}?allowed-host=${WEBHOOK_HOST}` } /** * Known per-bot wake sender keys. New bots need a matching kody.credentials * slot and a case here before wake can stamp-seal after republish. */ export const KNOWN_GROK_BOT_WAKE_SECRETS = [ 'grokBotWake.021d6409-01e7-4af0-a59c-4cbf2785cb50', 'grokBotWake.57072205-edee-46ed-b4fa-a24b657ef369', 'grokBotWake.58b84955-b713-4814-b0ca-b7982b4b5829', 'grokBotWake.60f91f03-6b17-4b1a-a590-529613c79977', 'grokBotWake.6519d644-b6ac-4e61-989c-3d96d0b63cca', 'grokBotWake.71e7550e-746d-417f-b253-05165975ff69', 'grokBotWake.775984a5-71ac-4e8c-bba3-b93de8ff6a06', 'grokBotWake.a08ff635-d146-4205-bb62-d538e9723d77', 'grokBotWake.a2600d4e-d242-4709-8a9f-f756ad2d7e82', 'grokBotWake.b5cf16d8-dde6-4d3d-b5b8-4452bfade775', 'grokBotWake.f835c77a-c93f-424e-a0f1-f191c2235e19', ] as const export type KnownGrokBotWakeSecret = (typeof KNOWN_GROK_BOT_WAKE_SECRETS)[number] /** * Return a literal stamped `{{secret:…}}` placeholder for a known wake key. * Scope suffixes are removed — org credentials assigned to the package replace * package/user `|scope=` buckets for the credential stamp. */ export function secretPlaceholder(secretName: string, _scope?: string | null): string { switch (secretName) { case 'grokBotWake.021d6409-01e7-4af0-a59c-4cbf2785cb50': return '{{secret:grokBotWake.021d6409-01e7-4af0-a59c-4cbf2785cb50}}' case 'grokBotWake.57072205-edee-46ed-b4fa-a24b657ef369': return '{{secret:grokBotWake.57072205-edee-46ed-b4fa-a24b657ef369}}' case 'grokBotWake.58b84955-b713-4814-b0ca-b7982b4b5829': return '{{secret:grokBotWake.58b84955-b713-4814-b0ca-b7982b4b5829}}' case 'grokBotWake.60f91f03-6b17-4b1a-a590-529613c79977': return '{{secret:grokBotWake.60f91f03-6b17-4b1a-a590-529613c79977}}' case 'grokBotWake.6519d644-b6ac-4e61-989c-3d96d0b63cca': return '{{secret:grokBotWake.6519d644-b6ac-4e61-989c-3d96d0b63cca}}' case 'grokBotWake.71e7550e-746d-417f-b253-05165975ff69': return '{{secret:grokBotWake.71e7550e-746d-417f-b253-05165975ff69}}' case 'grokBotWake.775984a5-71ac-4e8c-bba3-b93de8ff6a06': return '{{secret:grokBotWake.775984a5-71ac-4e8c-bba3-b93de8ff6a06}}' case 'grokBotWake.a08ff635-d146-4205-bb62-d538e9723d77': return '{{secret:grokBotWake.a08ff635-d146-4205-bb62-d538e9723d77}}' case 'grokBotWake.a2600d4e-d242-4709-8a9f-f756ad2d7e82': return '{{secret:grokBotWake.a2600d4e-d242-4709-8a9f-f756ad2d7e82}}' case 'grokBotWake.b5cf16d8-dde6-4d3d-b5b8-4452bfade775': return '{{secret:grokBotWake.b5cf16d8-dde6-4d3d-b5b8-4452bfade775}}' case 'grokBotWake.f835c77a-c93f-424e-a0f1-f191c2235e19': return '{{secret:grokBotWake.f835c77a-c93f-424e-a0f1-f191c2235e19}}' default: throw new Error( `Wake secret "${secretName}" is not stamp-declared. Add a kody.credentials slot and a secretPlaceholder case, then republish.`, ) } } export function parseBotRecord(value: unknown): BotRecord | null { let parsed = value if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed) } catch { return null } } if (!parsed || typeof parsed !== 'object') return null const row = parsed as Record const id = typeof row.id === 'string' ? row.id.trim() : '' const name = typeof row.name === 'string' ? row.name.trim() : '' const url = typeof row.url === 'string' ? row.url.trim() : '' const channelId = typeof row.channelId === 'string' ? row.channelId.trim() : '' if (!id || !name) return null const aliases = Array.isArray(row.aliases) ? row.aliases .filter((item): item is string => typeof item === 'string') .map((item) => normalizeName(item)) .filter(Boolean) : [normalizeName(name)] const uniqueAliases = Array.from(new Set(aliases)) if (!uniqueAliases.includes(normalizeName(name))) { uniqueAliases.push(normalizeName(name)) } return { id, name, aliases: uniqueAliases, url, secretName: secretNameFor(id), ...(channelId ? { channelId } : {}), createdAt: typeof row.createdAt === 'string' ? row.createdAt : new Date().toISOString(), updatedAt: typeof row.updatedAt === 'string' ? row.updatedAt : new Date().toISOString(), } } async function readAlias(normalized: string): Promise { const raw = await storageGet((key) => packageStorage().get(key), aliasKey(normalized)) if (typeof raw === 'string' && raw.trim()) return raw.trim() if (raw && typeof raw === 'object' && 'id' in raw) { const id = (raw as { id?: unknown }).id return typeof id === 'string' ? id.trim() : null } return null } export async function resolveBot(bot: string): Promise { const raw = bot.trim() if (!raw) return null const bucket = packageStorage() if (isUuidLike(raw)) { const byId = parseBotRecord(await storageGet((key) => bucket.get(key), botKey(raw))) if (byId) return byId } const idFromAlias = await readAlias(normalizeName(raw)) if (idFromAlias) { const byAlias = parseBotRecord( await storageGet((key) => bucket.get(key), botKey(idFromAlias)), ) if (byAlias) return byAlias } return parseBotRecord(await storageGet((key) => bucket.get(key), botKey(raw))) } export async function writeBotRecord(record: BotRecord): Promise { const bucket = packageStorage() const payload = JSON.stringify({ id: record.id, name: record.name, aliases: record.aliases, url: record.url, secretName: record.secretName, ...(record.channelId ? { channelId: record.channelId } : {}), createdAt: record.createdAt, updatedAt: record.updatedAt, }) await bucket.set(botKey(record.id), payload) for (const alias of record.aliases) { await bucket.set(aliasKey(alias), record.id) } } export async function deleteAlias(normalized: string): Promise { await packageStorage().delete(aliasKey(normalized)) } export async function deleteBotRecord(record: BotRecord): Promise { const bucket = packageStorage() await bucket.delete(botKey(record.id)) for (const alias of record.aliases) { await bucket.delete(aliasKey(alias)) } } function normalizeWakeSecrets(result: unknown): WakeSecretMeta[] { if (!result || typeof result !== 'object') return [] const secrets = (result as { secrets?: unknown }).secrets if (!Array.isArray(secrets)) return [] return secrets.flatMap((item) => { if (!item || typeof item !== 'object') return [] const row = item as Record const name = typeof row.name === 'string' ? row.name : '' if (!name.startsWith('grokBotWake.')) return [] const hosts = row.allowed_hosts ?? row.allowedHosts return [ { name, scope: typeof row.scope === 'string' ? row.scope : '', package_id: typeof row.package_id === 'string' ? row.package_id : typeof row.packageId === 'string' ? row.packageId : null, allowed_hosts: Array.isArray(hosts) ? hosts.filter((host): host is string => typeof host === 'string') : [], description: typeof row.description === 'string' ? row.description : null, updated_at: typeof row.updated_at === 'string' ? row.updated_at : typeof row.updatedAt === 'string' ? row.updatedAt : null, }, ] }) } /** * Direct `secretList` only. In the grok-bot package isolate (jobs, webhooks, * exports serving in package context) this returns package-scoped `grokBotWake.*` * keys. From ad hoc execute it usually does not — callers use stamp-optimistic * package-bucket fallbacks instead of subscription dispatch. */ export async function listWakeSecretsFromSecretList(): Promise { try { const seen = new Map() const listings = [plainClone(await kody.secretList({}))] for (const scope of ['package', 'user'] as const) { try { listings.push(plainClone(await kody.secretList({ scope }))) } catch { // Package-scoped listing may not be visible from execute yet. } } for (const listed of listings) { for (const secret of normalizeWakeSecrets(plainClone(listed))) { seen.set(`${secret.scope}:${secret.name}`, secret) } } return [...seen.values()] } catch { return null } } function hasPackageWakeSecrets(secrets: WakeSecretMeta[]): boolean { return secrets.some( (secret) => secret.scope === 'package' && secret.name.startsWith('grokBotWake.'), ) } function sortWakeSecrets(secrets: WakeSecretMeta[]): WakeSecretMeta[] { return [...secrets].sort( (a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope), ) } function metaFromPackageBucket(secretName: string, botName?: string): WakeSecretMeta { return { name: secretName, scope: 'package', package_id: SAVED_PACKAGE_ID, allowed_hosts: [WEBHOOK_HOST], description: botName ? `Grok Bot wake sender key for ${botName}` : 'Grok Bot wake sender key', updated_at: null, } } /** * Stamp-aligned / package-bucket existence for a single wake key. * Prefer authoritative listings; when listing is blind (execute), fall back to * stamp-optimistic package scope for wake (no subscription dispatch). */ export async function packageWakeSecretExists( secretName: string, ): Promise { const name = secretName.trim() if (!name) return false const listed = await listWakeSecretsFromSecretList() if (listed && listed.some((secret) => secret.name === name)) return true if (listed && hasPackageWakeSecrets(listed)) return false // Unbound listing: stamp fetch may still resolve package keys. if (name.startsWith('grokBotWake.')) return null return false } export async function listWakeSecrets(): Promise { const fromList = await listWakeSecretsFromSecretList() if (fromList === null) return null return sortWakeSecrets(fromList) } export function secretUseStatusFrom( secrets: WakeSecretMeta[] | null, secretName: string, ): SecretUseStatus { if (!secrets) { return { exists: false, usableScope: null, needsHostApproval: false, approvalUrl: null, allowedHosts: [], } } const matches = secrets.filter((secret) => secret.name === secretName) if (!matches.length) { return { exists: false, usableScope: null, needsHostApproval: false, approvalUrl: null, allowedHosts: [], } } const usable = matches.find((secret) => secret.allowed_hosts.includes(WEBHOOK_HOST)) const first = matches[0] return { exists: true, usableScope: usable?.scope ?? null, needsHostApproval: !usable, approvalUrl: first ? buildHostApprovalUrl({ scope: first.scope || 'user', secretName }) : null, allowedHosts: Array.from(new Set(matches.flatMap((secret) => secret.allowed_hosts))), } } export async function getSecretUseStatus(secretName: string): Promise { const listed = await listWakeSecrets() const fromList = secretUseStatusFrom(listed, secretName) if (fromList.exists) return fromList // Listing blind (execute): stamp-aligned fetch still resolves package-scoped // grokBotWake.* keys. Do not false-alarm "not saved". if (secretName.startsWith('grokBotWake.')) { return secretUseStatusFrom([metaFromPackageBucket(secretName)], secretName) } return fromList } export async function listKnownSecretNames(): Promise | null> { const secrets = await listWakeSecrets() if (!secrets) return null return new Set(secrets.map((secret) => secret.name)) } export async function isSecretPending(secretName: string): Promise { const status = await getSecretUseStatus(secretName) return !status.exists } /** * For `./secret-status`: direct secretList / listWakeSecrets only (no dispatch). */ export async function listWakeSecretsForStatus(): Promise { return (await listWakeSecrets()) ?? [] } function listEntries(listed: unknown): Array<{ key?: string; value?: unknown }> { if (Array.isArray(listed)) { return listed.filter((item) => item && typeof item === 'object') as Array<{ key?: string value?: unknown }> } if (listed && typeof listed === 'object') { const entries = (listed as { entries?: unknown }).entries if (Array.isArray(entries)) { return entries.filter((item) => item && typeof item === 'object') as Array<{ key?: string value?: unknown }> } } return [] } export async function listBotRecords(): Promise { const bucket = packageStorage() let listed: unknown = null try { listed = plainClone(await bucket.list({ prefix: BOT_PREFIX })) } catch (error) { const message = error instanceof Error ? error.message : String(error) if (!/Proxy could not be serialized/i.test(message)) throw error listed = null } const records: BotRecord[] = [] const seen = new Set() const keys: string[] = [] for (const entry of listEntries(listed)) { const key = typeof entry.key === 'string' ? entry.key : '' if (key && key.startsWith(BOT_PREFIX)) keys.push(key) } // Prefer per-key storageGet: list values for object rows can be Proxies. for (const key of keys) { const record = parseBotRecord(await storageGet((k) => bucket.get(k), key)) if (!record || seen.has(record.id)) continue seen.add(record.id) records.push(record) } return records } export function urlHostOf(url: string): string { if (!url) return '' try { return new URL(url).hostname } catch { return WEBHOOK_HOST } } export function missingBotError(bot: string): Error { return new Error( `No Grok Bot is registered for "${bot}". Call register({ id, name, url }) first.`, ) } export function missingSecretError(input: { name: string secretName: string saveUrl: string }): Error { return new Error( `Wake is refused until the sender key is saved for "${input.name}" as ${input.secretName}. Open ${input.saveUrl} and paste the key there (never in chat). Do not POST until that secret exists.`, ) } export function hostApprovalError(input: { name: string secretName: string approvalUrl: string }): Error { return new Error( `Wake is refused until ${WEBHOOK_HOST} is approved for "${input.name}" sender key ${input.secretName}. Open ${input.approvalUrl} and approve that host. Do not POST until the host is approved.`, ) }