Files
zeekay b758c19e05 feat(notion): Hanzo AI for Notion — OAuth integration + web app
Notion has no in-app UI-extension surface, so this ships a public OAuth
integration + REST v1 client and a small web app that operates on the
user's pages/databases. Four actions over a page:

- Summarize: recursively read blocks -> text, summarize via @hanzo/ai,
  append as a callout.
- Draft/expand: generate content continuing the page.
- Extract -> database: pull {task, owner, due} action items and create
  one database row per item.
- Ask: freeform Q&A over page context.

Architecture mirrors @hanzo/clio: pure, unit-tested request/blocks/chat
modules with thin DOM + http glue. The Notion client_secret AND bot token
stay server-side (server.ts) — the browser holds only an opaque session id
and proxies every Notion call through an allow-listed /notion/* proxy.
Model calls route through the createAiClient contract to api.hanzo.ai/v1.

104 vitest tests (OAuth shaping, REST wrappers + pagination, blocks<->text
both directions, @hanzo/ai request shaping, context assembly/truncation,
action-item parsing, proxy allow-list). tsc --noEmit clean; esbuild builds.
2026-07-03 19:38:28 -07:00

38 lines
1.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
HANZO_API_BASE_URL,
DEFAULT_MODEL,
NOTION_API_BASE_URL,
NOTION_VERSION,
notionApiRoot,
notionAuthorizeURL,
notionTokenURL,
} from '../src/config';
describe('config', () => {
it('points Hanzo at api.hanzo.ai with no /api/ prefix', () => {
expect(HANZO_API_BASE_URL).toBe('https://api.hanzo.ai');
expect(HANZO_API_BASE_URL).not.toContain('/api/');
});
it('defaults to a Zen model', () => {
expect(DEFAULT_MODEL).toBe('zen5');
});
it('pins a Notion API version', () => {
expect(NOTION_VERSION).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it('derives the Notion v1 root, authorize, and token URLs from the base', () => {
expect(notionApiRoot()).toBe('https://api.notion.com/v1');
expect(notionAuthorizeURL()).toBe('https://api.notion.com/v1/oauth/authorize');
expect(notionTokenURL()).toBe('https://api.notion.com/v1/oauth/token');
expect(NOTION_API_BASE_URL).toBe('https://api.notion.com');
});
it('strips a trailing slash on a base override', () => {
expect(notionApiRoot('https://example.test/')).toBe('https://example.test/v1');
expect(notionTokenURL('https://example.test//')).toBe('https://example.test/v1/oauth/token');
});
});