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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/scripts/init.ts +39 -23
  3. package/scripts/utils/templates.ts +53 -8
  4. package/sdk-version.json +1 -1
  5. package/templates/worker/.agents/INSTRUCTIONS.md +593 -0
  6. package/templates/worker/.agents/skills/auth-guide/SKILL.md +227 -0
  7. package/templates/worker/.agents/skills/sync/SKILL.md +368 -0
  8. package/templates/worker/.agents/skills/sync-debug/SKILL.md +101 -0
  9. package/templates/worker/.agents/skills/sync-guide/SKILL.md +253 -0
  10. package/templates/worker/.agents/skills/sync-guide/api-pagination-patterns.md +661 -0
  11. package/templates/worker/.agents/skills/sync-guide/examples/incremental-basic.ts +103 -0
  12. package/templates/worker/.agents/skills/sync-guide/examples/incremental-bimodal.ts +207 -0
  13. package/templates/worker/.agents/skills/sync-guide/examples/incremental-events.ts +132 -0
  14. package/templates/worker/.agents/skills/sync-guide/examples/replace-paginated.ts +79 -0
  15. package/templates/worker/.agents/skills/sync-guide/examples/replace-simple.ts +57 -0
  16. package/templates/worker/.agents/skills/sync-validate/SKILL.md +60 -0
  17. package/templates/worker/.claudeignore +2 -0
  18. package/templates/worker/.codexignore +2 -0
  19. package/templates/worker/.examples/automation-example.ts +60 -0
  20. package/templates/worker/.examples/oauth-example.ts +79 -0
  21. package/templates/worker/.examples/sync-example.ts +184 -0
  22. package/templates/worker/.examples/tool-example.ts +37 -0
  23. package/templates/worker/.examples/webhook-example.ts +66 -0
  24. package/templates/worker/README.md +765 -0
  25. package/templates/worker/_gitignore +6 -0
  26. package/templates/worker/docs/custom-tool.png +0 -0
  27. package/templates/worker/notionhq-workers-0.4.0.tgz +0 -0
  28. package/templates/worker/package.json +25 -0
  29. package/templates/worker/src/index.ts +8 -0
  30. package/templates/worker/tsconfig.json +16 -0
  31. package/templates/worker/views/empty/AGENTS.md +71 -0
  32. package/templates/worker/views/empty/README.md +10 -0
  33. package/templates/worker/views/empty/_gitignore +2 -0
  34. package/templates/worker/views/empty/custom_blocks.json +4 -0
  35. package/templates/worker/views/empty/index.html +15 -0
  36. package/templates/worker/views/empty/package.json +23 -0
  37. package/templates/worker/views/empty/src/index.css +33 -0
  38. package/templates/worker/views/empty/src/index.tsx +20 -0
  39. package/templates/worker/views/empty/tsconfig.json +17 -0
  40. 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
+ })