create-ncblock 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/init.ts +39 -23
- package/scripts/utils/templates.ts +53 -8
- package/sdk-version.json +1 -1
- package/templates/worker/.agents/INSTRUCTIONS.md +593 -0
- package/templates/worker/.agents/skills/auth-guide/SKILL.md +227 -0
- package/templates/worker/.agents/skills/sync/SKILL.md +368 -0
- package/templates/worker/.agents/skills/sync-debug/SKILL.md +101 -0
- package/templates/worker/.agents/skills/sync-guide/SKILL.md +253 -0
- package/templates/worker/.agents/skills/sync-guide/api-pagination-patterns.md +661 -0
- package/templates/worker/.agents/skills/sync-guide/examples/incremental-basic.ts +103 -0
- package/templates/worker/.agents/skills/sync-guide/examples/incremental-bimodal.ts +207 -0
- package/templates/worker/.agents/skills/sync-guide/examples/incremental-events.ts +132 -0
- package/templates/worker/.agents/skills/sync-guide/examples/replace-paginated.ts +79 -0
- package/templates/worker/.agents/skills/sync-guide/examples/replace-simple.ts +57 -0
- package/templates/worker/.agents/skills/sync-validate/SKILL.md +60 -0
- package/templates/worker/.claudeignore +2 -0
- package/templates/worker/.codexignore +2 -0
- package/templates/worker/.examples/automation-example.ts +60 -0
- package/templates/worker/.examples/oauth-example.ts +79 -0
- package/templates/worker/.examples/sync-example.ts +184 -0
- package/templates/worker/.examples/tool-example.ts +37 -0
- package/templates/worker/.examples/webhook-example.ts +66 -0
- package/templates/worker/README.md +765 -0
- package/templates/worker/_gitignore +6 -0
- package/templates/worker/docs/custom-tool.png +0 -0
- package/templates/worker/notionhq-workers-0.4.0.tgz +0 -0
- package/templates/worker/package.json +25 -0
- package/templates/worker/src/index.ts +8 -0
- package/templates/worker/tsconfig.json +16 -0
- package/templates/worker/views/empty/AGENTS.md +71 -0
- package/templates/worker/views/empty/README.md +10 -0
- package/templates/worker/views/empty/_gitignore +2 -0
- package/templates/worker/views/empty/custom_blocks.json +4 -0
- package/templates/worker/views/empty/index.html +15 -0
- package/templates/worker/views/empty/package.json +23 -0
- package/templates/worker/views/empty/src/index.css +33 -0
- package/templates/worker/views/empty/src/index.tsx +20 -0
- package/templates/worker/views/empty/tsconfig.json +17 -0
- package/templates/worker/views/empty/vite.config.ts +7 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automations are only available in a private alpha.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Worker } from "@notionhq/workers"
|
|
6
|
+
|
|
7
|
+
const worker = new Worker()
|
|
8
|
+
export default worker
|
|
9
|
+
|
|
10
|
+
type RichTextProperty = {
|
|
11
|
+
type: "rich_text"
|
|
12
|
+
rich_text: Array<{ plain_text: string }>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Example automation that sends an email based on a database page property.
|
|
17
|
+
*
|
|
18
|
+
* This automation:
|
|
19
|
+
* 1. Reads an email address from a page property
|
|
20
|
+
* 2. Sends an email to that address
|
|
21
|
+
* 3. Updates the page to indicate the email has been sent
|
|
22
|
+
*/
|
|
23
|
+
worker.automation("sendEmailAutomation", {
|
|
24
|
+
title: "Send Email Automation",
|
|
25
|
+
description: "Reads an email address from a database page and sends an email",
|
|
26
|
+
execute: async (event, { notion }) => {
|
|
27
|
+
const { pageId, pageData } = event
|
|
28
|
+
// Extract email from the page data
|
|
29
|
+
const emailProperty = pageData?.properties?.Email as
|
|
30
|
+
| RichTextProperty
|
|
31
|
+
| undefined
|
|
32
|
+
|
|
33
|
+
// Extract text content from the property
|
|
34
|
+
let emailValue = ""
|
|
35
|
+
if (emailProperty?.type === "rich_text") {
|
|
36
|
+
emailValue = emailProperty.rich_text.map(rt => rt.plain_text).join("")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle empty email or pageId
|
|
40
|
+
if (!emailValue || !pageId) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await sendEmail(emailValue)
|
|
45
|
+
|
|
46
|
+
// Update the page to indicate the email has been sent
|
|
47
|
+
await notion.pages.update({
|
|
48
|
+
page_id: pageId,
|
|
49
|
+
properties: {
|
|
50
|
+
EmailSent: {
|
|
51
|
+
checkbox: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
async function sendEmail(email: string): Promise<void> {
|
|
59
|
+
console.log(`Sending email to ${email}`)
|
|
60
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Worker } from "@notionhq/workers"
|
|
2
|
+
import * as Schema from "@notionhq/workers/schema"
|
|
3
|
+
import { j } from "@notionhq/workers/schema-builder"
|
|
4
|
+
|
|
5
|
+
const worker = new Worker()
|
|
6
|
+
export default worker
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OAuth capabilities let your worker access third-party APIs.
|
|
10
|
+
*
|
|
11
|
+
* After deploying your worker, start OAuth from the CLI:
|
|
12
|
+
*
|
|
13
|
+
* ntn workers oauth start <capabilityKey>
|
|
14
|
+
*
|
|
15
|
+
* Where `capabilityKey` is the OAuth capability's key (see `ntn workers capabilities list`).
|
|
16
|
+
* Once OAuth completes, the worker runtime exposes the access token via an
|
|
17
|
+
* environment variable and `accessToken()` reads it for you.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Option 1: Notion-managed provider (recommended when available).
|
|
21
|
+
// Notion owns the OAuth app credentials and the backend has pre-configured provider settings.
|
|
22
|
+
// Notion-managed providers are only available in a private alpha.
|
|
23
|
+
const googleAuth = worker.oauth("googleAuth", {
|
|
24
|
+
name: "google-calendar",
|
|
25
|
+
provider: "google",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Option 2: User-managed provider (you own the OAuth app credentials).
|
|
29
|
+
// Keep client credentials in worker secrets and read them from `process.env`.
|
|
30
|
+
// Generally available.
|
|
31
|
+
const myCustomAuth = worker.oauth("myCustomAuth", {
|
|
32
|
+
name: "my-custom-provider",
|
|
33
|
+
authorizationEndpoint: "https://provider.example.com/oauth/authorize",
|
|
34
|
+
tokenEndpoint: "https://provider.example.com/oauth/token",
|
|
35
|
+
scope: "read write",
|
|
36
|
+
clientId: "1234567890",
|
|
37
|
+
clientSecret: process.env.MY_CUSTOM_OAUTH_CLIENT_SECRET ?? "",
|
|
38
|
+
authorizationParams: {
|
|
39
|
+
access_type: "offline",
|
|
40
|
+
prompt: "consent",
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Use the OAuth handles in your capabilities
|
|
45
|
+
const calendarEvents = worker.database("calendarEvents", {
|
|
46
|
+
type: "managed",
|
|
47
|
+
initialTitle: "Calendar Events",
|
|
48
|
+
primaryKeyProperty: "Event ID",
|
|
49
|
+
schema: {
|
|
50
|
+
properties: {
|
|
51
|
+
Title: Schema.title(),
|
|
52
|
+
"Event ID": Schema.richText(),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
worker.sync("googleCalendarSync", {
|
|
58
|
+
database: calendarEvents,
|
|
59
|
+
execute: async () => {
|
|
60
|
+
// Get the OAuth access token
|
|
61
|
+
const token = await googleAuth.accessToken()
|
|
62
|
+
|
|
63
|
+
// Use token to fetch from Google Calendar API
|
|
64
|
+
console.log("Using Google token:", `${token.slice(0, 10)}...`)
|
|
65
|
+
|
|
66
|
+
return { changes: [], hasMore: false }
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
worker.tool("customApiTool", {
|
|
71
|
+
title: "Custom API Tool",
|
|
72
|
+
description: "Calls a custom API using OAuth",
|
|
73
|
+
schema: j.object({}),
|
|
74
|
+
execute: async () => {
|
|
75
|
+
const token = await myCustomAuth.accessToken()
|
|
76
|
+
console.log("Using custom provider token:", `${token.slice(0, 10)}...`)
|
|
77
|
+
return { success: true }
|
|
78
|
+
},
|
|
79
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: syncing projects and tasks from an external API to Notion.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - Database declarations (hoisted from sync config)
|
|
6
|
+
* - Pacers for rate limiting upstream API requests
|
|
7
|
+
* - Relations between databases
|
|
8
|
+
* - Backfill + delta sync pattern
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Worker } from "@notionhq/workers"
|
|
12
|
+
import * as Builder from "@notionhq/workers/builder"
|
|
13
|
+
import * as Schema from "@notionhq/workers/schema"
|
|
14
|
+
|
|
15
|
+
const worker = new Worker()
|
|
16
|
+
export default worker
|
|
17
|
+
|
|
18
|
+
// -- Pacer: rate-limit requests to the upstream API --
|
|
19
|
+
// Research the API's rate limits and declare them here.
|
|
20
|
+
// If multiple syncs share a pacer, the budget is apportioned evenly.
|
|
21
|
+
const exampleApi = worker.pacer("exampleApi", {
|
|
22
|
+
allowedRequests: 10, // 10 requests
|
|
23
|
+
intervalMs: 1000, // per second
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// -- Databases --
|
|
27
|
+
|
|
28
|
+
const projects = worker.database("projects", {
|
|
29
|
+
type: "managed",
|
|
30
|
+
initialTitle: "Projects",
|
|
31
|
+
primaryKeyProperty: "Project ID",
|
|
32
|
+
schema: {
|
|
33
|
+
properties: {
|
|
34
|
+
"Project Name": Schema.title(),
|
|
35
|
+
"Project ID": Schema.richText(),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const tasks = worker.database("tasks", {
|
|
41
|
+
type: "managed",
|
|
42
|
+
initialTitle: "Tasks",
|
|
43
|
+
primaryKeyProperty: "Task ID",
|
|
44
|
+
schema: {
|
|
45
|
+
properties: {
|
|
46
|
+
"Task Name": Schema.title(),
|
|
47
|
+
"Task ID": Schema.richText(),
|
|
48
|
+
Status: Schema.select([
|
|
49
|
+
{ name: "Open", color: "blue" },
|
|
50
|
+
{ name: "In Progress", color: "yellow" },
|
|
51
|
+
{ name: "Done", color: "green" },
|
|
52
|
+
]),
|
|
53
|
+
Project: Schema.relation("projectsSync", {
|
|
54
|
+
twoWay: true,
|
|
55
|
+
relatedPropertyName: "Tasks",
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// -- Simple replace sync for projects (small dataset) --
|
|
62
|
+
|
|
63
|
+
worker.sync("projectsSync", {
|
|
64
|
+
database: projects,
|
|
65
|
+
mode: "replace",
|
|
66
|
+
schedule: "1h",
|
|
67
|
+
execute: async state => {
|
|
68
|
+
const page = state?.page ?? 1
|
|
69
|
+
await exampleApi.wait()
|
|
70
|
+
const { items, hasMore } = await fetchProjects(page)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
changes: items.map(item => ({
|
|
74
|
+
type: "upsert" as const,
|
|
75
|
+
key: item.id,
|
|
76
|
+
properties: {
|
|
77
|
+
"Project Name": Builder.title(item.name),
|
|
78
|
+
"Project ID": Builder.richText(item.id),
|
|
79
|
+
},
|
|
80
|
+
})),
|
|
81
|
+
hasMore,
|
|
82
|
+
nextState: hasMore ? { page: page + 1 } : undefined,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// -- Backfill + delta sync pair for tasks --
|
|
88
|
+
|
|
89
|
+
// Backfill: paginates the full upstream dataset.
|
|
90
|
+
// Schedule: manual. To run:
|
|
91
|
+
// ntn workers sync state reset tasksBackfill
|
|
92
|
+
// ntn workers sync trigger tasksBackfill
|
|
93
|
+
// Replace mode: mark-and-sweep deletes records no longer upstream.
|
|
94
|
+
worker.sync("tasksBackfill", {
|
|
95
|
+
database: tasks,
|
|
96
|
+
mode: "replace",
|
|
97
|
+
schedule: "manual",
|
|
98
|
+
execute: async state => {
|
|
99
|
+
const page = state?.page ?? 1
|
|
100
|
+
await exampleApi.wait()
|
|
101
|
+
const { items, hasMore } = await fetchAllTasks(page)
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
changes: items.map(item => ({
|
|
105
|
+
type: "upsert" as const,
|
|
106
|
+
key: item.id,
|
|
107
|
+
properties: {
|
|
108
|
+
"Task Name": Builder.title(item.name),
|
|
109
|
+
"Task ID": Builder.richText(item.id),
|
|
110
|
+
Status: Builder.select(item.status),
|
|
111
|
+
Project: [Builder.relation(item.projectId)],
|
|
112
|
+
},
|
|
113
|
+
})),
|
|
114
|
+
hasMore,
|
|
115
|
+
nextState: hasMore ? { page: page + 1 } : undefined,
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Delta: fetches only recent changes.
|
|
121
|
+
// Schedule: runs every 5 minutes to keep Notion up to date.
|
|
122
|
+
// Incremental mode: only returns changes since the last cursor.
|
|
123
|
+
worker.sync("tasksDelta", {
|
|
124
|
+
database: tasks,
|
|
125
|
+
mode: "incremental",
|
|
126
|
+
schedule: "5m",
|
|
127
|
+
execute: async state => {
|
|
128
|
+
const cursor = state?.cursor
|
|
129
|
+
await exampleApi.wait()
|
|
130
|
+
const { items, nextCursor } = await fetchTaskChanges(cursor)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
changes: items.map(item => ({
|
|
134
|
+
type: "upsert" as const,
|
|
135
|
+
key: item.id,
|
|
136
|
+
properties: {
|
|
137
|
+
"Task Name": Builder.title(item.name),
|
|
138
|
+
"Task ID": Builder.richText(item.id),
|
|
139
|
+
Status: Builder.select(item.status),
|
|
140
|
+
Project: [Builder.relation(item.projectId)],
|
|
141
|
+
},
|
|
142
|
+
})),
|
|
143
|
+
hasMore: Boolean(nextCursor),
|
|
144
|
+
nextState: nextCursor ? { cursor: nextCursor } : undefined,
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// -- Placeholder functions (replace with real API calls) --
|
|
150
|
+
|
|
151
|
+
async function fetchProjects(_page: number) {
|
|
152
|
+
return {
|
|
153
|
+
items: [{ id: "proj-1", name: "Example Project" }],
|
|
154
|
+
hasMore: false,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function fetchAllTasks(_page: number) {
|
|
159
|
+
return {
|
|
160
|
+
items: [
|
|
161
|
+
{
|
|
162
|
+
id: "task-1",
|
|
163
|
+
name: "Write docs",
|
|
164
|
+
status: "Open",
|
|
165
|
+
projectId: "proj-1",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
hasMore: false,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function fetchTaskChanges(_cursor: string | undefined) {
|
|
173
|
+
return {
|
|
174
|
+
items: [
|
|
175
|
+
{
|
|
176
|
+
id: "task-1",
|
|
177
|
+
name: "Write docs",
|
|
178
|
+
status: "Done",
|
|
179
|
+
projectId: "proj-1",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
nextCursor: null as string | null,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools are generally available.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Worker } from "@notionhq/workers"
|
|
6
|
+
import { j } from "@notionhq/workers/schema-builder"
|
|
7
|
+
|
|
8
|
+
const worker = new Worker()
|
|
9
|
+
export default worker
|
|
10
|
+
|
|
11
|
+
worker.tool("myTool", {
|
|
12
|
+
title: "My Tool",
|
|
13
|
+
// Description of what this tool does - shown to the AI agent
|
|
14
|
+
description: "Search for items by keyword or ID",
|
|
15
|
+
// Use the schema builder to define input — it auto-sets required and
|
|
16
|
+
// additionalProperties, and provides type inference.
|
|
17
|
+
schema: j.object({
|
|
18
|
+
query: j.string().describe("The search query").nullable(),
|
|
19
|
+
limit: j.number().describe("Maximum number of results").nullable(),
|
|
20
|
+
}),
|
|
21
|
+
// Optional: schema for the output the tool returns
|
|
22
|
+
outputSchema: j.object({
|
|
23
|
+
results: j.array(j.string()),
|
|
24
|
+
}),
|
|
25
|
+
// The function that executes when the tool is called
|
|
26
|
+
execute: async (input, { notion: _notion }) => {
|
|
27
|
+
// Destructure input with default values
|
|
28
|
+
const { query: _query, limit: _limit = 10 } = input
|
|
29
|
+
|
|
30
|
+
// Perform your logic here
|
|
31
|
+
// Example: search your data source using the query and limit
|
|
32
|
+
const results: string[] = []
|
|
33
|
+
|
|
34
|
+
// Return data matching your outputSchema (if provided)
|
|
35
|
+
return { results }
|
|
36
|
+
},
|
|
37
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This example shows how to verify GitHub webhook signatures using
|
|
3
|
+
* HMAC-SHA256. Set GITHUB_WEBHOOK_SECRET via `ntn workers env set`.
|
|
4
|
+
*
|
|
5
|
+
* After 5 consecutive WebhookVerificationError throws, the platform
|
|
6
|
+
* short-circuits and rejects all incoming requests without executing
|
|
7
|
+
* the handler. Redeploying the worker resets the counter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { WebhookVerificationError, Worker } from "@notionhq/workers"
|
|
11
|
+
import crypto from "crypto"
|
|
12
|
+
|
|
13
|
+
const worker = new Worker()
|
|
14
|
+
export default worker
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Verify a GitHub webhook signature.
|
|
18
|
+
* GitHub sends the HMAC-SHA256 signature in the X-Hub-Signature-256 header
|
|
19
|
+
* as "sha256={hex}". The raw body must be used for verification.
|
|
20
|
+
*/
|
|
21
|
+
function verifyGitHubSignature(
|
|
22
|
+
rawBody: string,
|
|
23
|
+
headers: Record<string, string>,
|
|
24
|
+
): void {
|
|
25
|
+
const secret = process.env.GITHUB_WEBHOOK_SECRET
|
|
26
|
+
if (!secret) {
|
|
27
|
+
throw new WebhookVerificationError("GITHUB_WEBHOOK_SECRET not configured")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const signature = headers["x-hub-signature-256"]
|
|
31
|
+
if (!signature?.startsWith("sha256=")) {
|
|
32
|
+
throw new WebhookVerificationError("Invalid GitHub signature")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const expected = `sha256=${crypto
|
|
36
|
+
.createHmac("sha256", secret)
|
|
37
|
+
.update(rawBody)
|
|
38
|
+
.digest("hex")}`
|
|
39
|
+
|
|
40
|
+
if (signature.length !== expected.length) {
|
|
41
|
+
throw new WebhookVerificationError("Invalid GitHub signature")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
45
|
+
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
46
|
+
throw new WebhookVerificationError("Invalid GitHub signature")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
worker.webhook("onGithubPush", {
|
|
51
|
+
title: "GitHub Push Webhook",
|
|
52
|
+
description: "Handles push events from GitHub repositories",
|
|
53
|
+
execute: async events => {
|
|
54
|
+
for (const event of events) {
|
|
55
|
+
verifyGitHubSignature(event.rawBody, event.headers)
|
|
56
|
+
|
|
57
|
+
// Signature verified — safe to process
|
|
58
|
+
const body = event.body
|
|
59
|
+
const ref = body.ref as string | undefined
|
|
60
|
+
const pusher = body.pusher as { name?: string } | undefined
|
|
61
|
+
console.log(
|
|
62
|
+
`Verified push to ${ref ?? "unknown"} by ${pusher?.name ?? "unknown"}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
})
|