@keystrokehq/notion
v0.0.15
Published
Official Notion integration for Keystroke.
Readme
@keystrokehq/notion
Official Notion integration for Keystroke.
Import Paths
Use explicit subpath imports. The package root is intentionally non-canonical.
import { notion } from '@keystrokehq/notion/connection';
import { copyPageContent, createPage, getPage } from '@keystrokehq/notion/pages';
import { listBlockChildren } from '@keystrokehq/notion/blocks';
import { queryDataSource } from '@keystrokehq/notion/data-sources';
import { listComments, createComment } from '@keystrokehq/notion/comments';
import { getMe, listUsers } from '@keystrokehq/notion/users';
import { searchWorkspace } from '@keystrokehq/notion/search';
import { webhooks, polling } from '@keystrokehq/notion/triggers';Connection Model
The public connection surface normalizes two auth paths into the same runtime credential shape:
- OAuth connection through Keystroke-managed Notion public integrations
- Manual internal-integration token entry
Both resolve to:
{
NOTION_ACCESS_TOKEN: string;
}In practice:
- OAuth is the default path when users run
keystroke connect notion - Manual token entry is for internal Notion integrations that already have a bearer token
Core Surfaces
pages: page reads, property reads, create, content copy, updates, trash, restoreblocks: block reads, child listing, append, update, deletedata-sources: canonical data-source discovery and query flowscomments: page comments and discussion repliesusers:me, single-user lookup, workspace user listingsearch: workspace-wide page and data-source discoverytriggers: webhook-first direct bindings plus polling parity helpers
data_source is the canonical runtime model in this package. Database discovery is still available for the upgrade path, but author-facing operations prefer data_source IDs over legacy database query endpoints.
For duplication-style workflows, pair createPage with copyPageContent. The helper recreates supported block trees level by level so it stays inside Notion's append limits. Pages containing API-unsupported block types such as child_page, child_database, or link_preview should use a template-driven flow instead.
Trigger Examples
import { webhooks, polling } from '@keystrokehq/notion/triggers';
const pageCreated = webhooks.pageCreated({
name: 'Notion Page Created',
transform: (event) => ({
pageId: event.entityId,
eventType: event.eventType,
}),
});
const pageUpdated = polling.pageUpdated({
name: 'Notion Page Updated',
pageId: 'notion-page-id',
schedule: '*/10 * * * *',
});Webhook bindings emit metadata-first payloads. They intentionally do not auto-hydrate full Notion objects. Use the page, comment, or data-source operations to fetch current state when the workflow needs it.
Platform Admin Setup
Notion webhook subscriptions are configured in Notion, not through the public API. A Keystroke platform admin must complete the bootstrap:
- Create or update the Keystroke-managed Notion public integration in Notion.
- Enable the minimum capabilities the shipped surface expects:
- Read content
- Insert content
- Update content
- Read comment
- Insert comment
- User information
- Configure the OAuth redirect URL to Keystroke's
/api/v1/connections/callback. - Configure the webhook destination URL to Keystroke's
/api/v1/webhooks/{orgId}/notion. - Store the Notion OAuth app credentials in the internal
notion-appcredential set. - Store the Notion webhook verification token in the internal
notion-webhookcredential set.
Provider Caveats
- Notion OAuth is provider-specific. The Keystroke server uses a Notion-specific auth URL builder and token exchange flow instead of the generic OAuth strategy.
- Notion webhook payloads are sparse. Event IDs, entity IDs, authors, and event metadata are the contract; workflows should fetch fresh objects when they need content.
- Notion write operations are approval-gated by default.
Testing
Coverage map: See
TEST_MATRIX.mdfor the full list of operations, webhook/polling triggers, and credential/server boundaries and what each must prove (success, failure, safety, live).Unit / contract:
pnpm test:unit— exercises.run()paths with mocked@notionhq/client, triggers, client helpers, events, verification, and schemas.Live Notion (
test:int):pnpm test:int— package-level integration suites in*.int.test.ts. Credentials follow a two-tier waterfall via the sharedresolveLiveCredentialsForCredentialSethelper from@keystrokehq/test-utils:- Env vars (CI-friendly):
NOTION_TEST_ACCESS_TOKENorNOTION_TEST_INTEGRATION_TOKENin.env.test. - Saved Keystroke connection (recommended for local dev): run
pnpm cli auth+pnpm cli connect notiononce. The suites then resolve credentials from the Keystroke server (POST /api/v1/credentials/resolve), so any OAuth refresh happens server-side and tests never touch the refresh token.
Sandbox parent page is auto-discovered. The integration test harness calls Notion's search API in
beforeAllto pick the first page the integration has access to as the sandbox parent. Just share at least one page with your Notion integration (during the OAuth consent, or later via the page share menu) and the page/block/comment/trigger suites will run end-to-end. SetNOTION_TEST_PARENT_PAGE_IDto pin a specific page (recommended in CI for deterministic runs).Other fixture IDs (
NOTION_TEST_DATA_SOURCE_ID,NOTION_TEST_PAGE_ID,NOTION_TEST_PAGE_PROPERTY_ID, etc.) remain opt-in env overrides; tests that need themit.skipIfout cleanly when they are absent. Full list in the root.env.test.example. Without credentials, the suites skip the whole suite with a warning — no flag flipping required.- Env vars (CI-friendly):
Server OAuth: Database-backed routes are covered under
@keystroke/server(connections/.../initiate,connections/callback). Runturbo run test:int --filter=@keystroke/serverwith a local Postgres (see root.env.test.example).
