@firedrill-tools/notion 0.1.1
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/LICENSE +201 -0
- package/README.md +402 -0
- package/firedrill/agent.target.json +17 -0
- package/firedrill/baseline.scenario.json +5 -0
- package/firedrill/bounded.scenario.json +19 -0
- package/firedrill/conformance.suite.json +23 -0
- package/firedrill/notion-bounded.drill.json +318 -0
- package/firedrill/notion-byte-budget.drill.json +116 -0
- package/firedrill/notion-mcp-aliases.drill.json +150 -0
- package/firedrill/notion-page-authoring.drill.json +254 -0
- package/firedrill/notion-rate-limited.drill.json +118 -0
- package/firedrill/notion-schema-growth.drill.json +88 -0
- package/firedrill/notion-scope-agent-only.drill.json +131 -0
- package/firedrill/notion-scope-auditor.drill.json +86 -0
- package/firedrill/notion-scope-board-bot.drill.json +128 -0
- package/firedrill/notion-scope-notes-bot.drill.json +303 -0
- package/firedrill/notion-scope-stranger.drill.json +773 -0
- package/firedrill/notion-task-triage.drill.json +277 -0
- package/firedrill/notion-trash-and-restore.drill.json +186 -0
- package/firedrill/notion-update-lost.drill.json +88 -0
- package/firedrill/notion-workspace-read.drill.json +258 -0
- package/firedrill/notion-write-unavailable.drill.json +161 -0
- package/firedrill/rate-limited.scenario.json +11 -0
- package/firedrill/tools/notion/app/assets/ATTRIBUTION.md +35 -0
- package/firedrill/tools/notion/app/assets/fonts/OFL.txt +93 -0
- package/firedrill/tools/notion/app/assets/fonts/inter-latin.woff2 +0 -0
- package/firedrill/tools/notion/app/assets/notion-wordmark.svg +1 -0
- package/firedrill/tools/notion/app/assets/notion.svg +1 -0
- package/firedrill/tools/notion/app/site/app.js +797 -0
- package/firedrill/tools/notion/app/site/assets/fonts/inter-latin.woff2 +0 -0
- package/firedrill/tools/notion/app/site/assets/notion-wordmark.svg +1 -0
- package/firedrill/tools/notion/app/site/assets/notion.svg +1 -0
- package/firedrill/tools/notion/app/site/chrome.js +104 -0
- package/firedrill/tools/notion/app/site/cover-picker.js +83 -0
- package/firedrill/tools/notion/app/site/database.js +648 -0
- package/firedrill/tools/notion/app/site/editors.js +320 -0
- package/firedrill/tools/notion/app/site/format-bar.js +97 -0
- package/firedrill/tools/notion/app/site/icons.js +131 -0
- package/firedrill/tools/notion/app/site/index.html +125 -0
- package/firedrill/tools/notion/app/site/page.js +826 -0
- package/firedrill/tools/notion/app/site/rich.js +159 -0
- package/firedrill/tools/notion/app/site/state.js +170 -0
- package/firedrill/tools/notion/app/site/styles.css +826 -0
- package/firedrill/tools/notion/app/site/ui.js +418 -0
- package/firedrill/tools/notion/behavior.mjs +1123 -0
- package/firedrill/tools/notion/lib/blocks.mjs +371 -0
- package/firedrill/tools/notion/lib/identity.mjs +123 -0
- package/firedrill/tools/notion/lib/ids.mjs +63 -0
- package/firedrill/tools/notion/lib/json-depth.mjs +26 -0
- package/firedrill/tools/notion/lib/markdown.mjs +381 -0
- package/firedrill/tools/notion/lib/properties.mjs +513 -0
- package/firedrill/tools/notion/lib/query.mjs +272 -0
- package/firedrill/tools/notion/lib/render.mjs +137 -0
- package/firedrill/tools/notion/lib/rich-text.mjs +134 -0
- package/firedrill/tools/notion/lib/size.mjs +44 -0
- package/firedrill/tools/notion/lib/state.mjs +192 -0
- package/firedrill/tools/notion/lib/wire.mjs +89 -0
- package/firedrill/tools/notion/notion.tool.json +9837 -0
- package/firedrill/update-lost.scenario.json +11 -0
- package/firedrill/world.json +7039 -0
- package/firedrill/write-unavailable.scenario.json +11 -0
- package/firedrill.json +5 -0
- package/package.json +63 -0
- package/starter.json +6482 -0
- package/test/conformance.mjs +1186 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Reload Tech Inc.
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# @firedrill-tools/notion
|
|
2
|
+
|
|
3
|
+
A synthetic **Notion workspace** for [Firedrill](https://firedrill.run): one fictional workspace with a wiki page tree,
|
|
4
|
+
three databases (the 2025-09-03 *database container + data source* model), blocks, comments and users, exposed through a
|
|
5
|
+
bounded subset of the Notion REST API (`/v1/...`, `Notion-Version: 2025-09-03` shapes plus the 2026-03-11 page-markdown
|
|
6
|
+
endpoints) and through the tool names of the open-source Notion MCP server. Agents that talk to Notion through
|
|
7
|
+
`@notionhq/client`-style REST calls or over MCP can be exercised against it without a workspace, an integration token or
|
|
8
|
+
network access.
|
|
9
|
+
|
|
10
|
+
Everything is computed from the world's state: ids come from a counter row, timestamps from Firedrill's virtual clock,
|
|
11
|
+
visibility and capabilities from the calling integration. Nothing here contacts Notion; no notification, e-mail or webhook
|
|
12
|
+
is ever sent, and no `notion.so` URL in a response resolves anywhere.
|
|
13
|
+
|
|
14
|
+
Package id `notion`, version `0.1.0`, engine `>=0.1.0 <0.2.0`, license Apache-2.0. The package ships a **browser app**
|
|
15
|
+
(a recreation of the Notion web client over the same operations — see *App*) next to the REST routes and MCP aliases.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
firedrill tool add @firedrill-tools/notion --install
|
|
21
|
+
firedrill serve --scenario baseline --no-open
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`firedrill serve` prints the HTTP and MCP endpoints and their tokens. A new world receives the starter data and the
|
|
25
|
+
grants below. Existing worlds do not change silently: add the grants and (optionally) the `starter.json` rows to your
|
|
26
|
+
own world or scenario.
|
|
27
|
+
|
|
28
|
+
### Grants and identities
|
|
29
|
+
|
|
30
|
+
Every actor that should use the Tool needs the grants for the operations it may call (`packageId: "notion"`):
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
users.me users.list users.get search
|
|
34
|
+
pages.create pages.retrieve pages.update pages.retrieve-property pages.move pages.retrieve-markdown pages.update-markdown
|
|
35
|
+
databases.create databases.retrieve data-sources.retrieve data-sources.query data-sources.update
|
|
36
|
+
blocks.retrieve blocks.children.list blocks.children.append blocks.update blocks.delete
|
|
37
|
+
comments.create comments.list workspace.context workspace.trash
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Two optional actor **attributes** model *which internal integration* is calling and *which person* it acts for:
|
|
41
|
+
|
|
42
|
+
| attribute | meaning |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `integrationId` (optional) | Row id in the `integrations` namespace. Selects the bot user returned by `users.me`, the capability set (`content`, `comments`, `user_information`) and the visibility scope (`workspace` or a list of shared root pages/databases). **Absent = the first `integrations` row in row-id order** — the seeded workspace-wide *Firedrill Agent* — so a grant-only actor created by `firedrill tool add --install` sees the whole starter workspace. An id that names no row fails every operation with `UNAUTHORIZED` (HTTP 401 `unauthorized`, "The bearer token is not valid."). |
|
|
45
|
+
| `userId` (optional) | Row id of a **person** user. When present, writes are attributed (`created_by`, `last_edited_by`, comment author with `display_name.type: "user"`) to that person and `workspace.context.user` returns them. When absent, writes are attributed to the integration's bot user, exactly as the real API does. An id that is not a person row also fails `UNAUTHORIZED`. |
|
|
46
|
+
|
|
47
|
+
The seeded integrations are `fd000000-0000-4000-8000-000700000001` **Firedrill Agent** (`content: read_update_insert`,
|
|
48
|
+
`comments: read_insert`, `user_information: with_emails`, workspace-wide), `…000700000002` **Notes Sync** (`content: read`,
|
|
49
|
+
`comments: read`, `user_information: none`, shared only with the *Meeting notes* subtree) and `…000700000003` **Status
|
|
50
|
+
Board** (`content: read`, `comments: none`, `user_information: without_emails`, workspace-wide). The conformance world
|
|
51
|
+
(`firedrill/world.json`) shows every combination as actors `member`, `agent-only`, `notes-bot`, `board-bot`, `stranger`
|
|
52
|
+
and `auditor`.
|
|
53
|
+
|
|
54
|
+
Rules the behavior enforces: an object is visible only if it, or an ancestor in its parent chain, is one of the
|
|
55
|
+
integration's shared roots (Notion never distinguishes "missing" from "not shared": both answer `object_not_found`);
|
|
56
|
+
trashed objects stay readable by id (`in_trash: true`) but leave search, queries and children listings; every content
|
|
57
|
+
read needs `content ≥ read`, updates/moves/markdown edits/block edits/deletes `read_update`, creating pages, databases and
|
|
58
|
+
blocks `read_update_insert`, listing comments `comments ≥ read`, creating them `read_insert`, and `users.list` /
|
|
59
|
+
`users.get` need `user_information ≠ none` (`person.email` appears only with `with_emails`). Violations answer
|
|
60
|
+
`RESTRICTED_RESOURCE` (HTTP 403 `restricted_resource`). Framework grants are checked before any of this: an actor
|
|
61
|
+
without the grant is denied by Firedrill (HTTP 403 with a Notion-shaped `restricted_resource` body).
|
|
62
|
+
|
|
63
|
+
### Connections
|
|
64
|
+
|
|
65
|
+
Two connection recipes are declared: `notion-sdk` (`NOTION_BASE_URL` = the HTTP binding, `NOTION_TOKEN` = its token, to be
|
|
66
|
+
passed as `baseUrl` / `auth` to `@notionhq/client` in a test-only process) and `notion-mcp` (`NOTION_MCP_URL` /
|
|
67
|
+
`NOTION_MCP_TOKEN` for the MCP binding). The values are isolated world tokens for test processes only. Neither client has
|
|
68
|
+
been exercised against this package yet (see *Compatibility*).
|
|
69
|
+
|
|
70
|
+
## Starting data
|
|
71
|
+
|
|
72
|
+
`starter.json` (and `firedrill/world.json`) contain 103 fictional rows, virtual time `2026-09-14T09:00:00Z`: the workspace
|
|
73
|
+
**Halvard Robotics** (`@halvard.example`), five people (Ines Okafor, Tomas Lindqvist, Priya Raman, Marco Belli and the
|
|
74
|
+
guest Yuki Sato), three bot users, three integrations, a wiki of nine pages (*Halvard Robotics Home* at the workspace root
|
|
75
|
+
→ *Engineering* → *Onboarding guide* and the empty *Empty scratchpad*; *Design principles* with 13 top-level blocks
|
|
76
|
+
including a toggle with children, numbered and bulleted lists, a quote, a callout, a code block, a divider and a bookmark;
|
|
77
|
+
*Meeting notes* → two *Weekly sync* pages; and *Roadmap 2025* in the trash), three databases — **Projects** (5 rows;
|
|
78
|
+
status, people, date range, relation to Tasks), **Tasks** (14 rows; status with groups, assignee, due date, priority
|
|
79
|
+
select, tags multi-select, checkbox, number, url, rich text with a user mention, relation to Projects, computed
|
|
80
|
+
`created_by` / `last_edited_time`) and the empty inline **Decisions log** — 49 blocks and 6 comments in four discussions.
|
|
81
|
+
The data deliberately covers an unassigned task, a task due "today", an overdue one, one without a due date, an `Urgent`
|
|
82
|
+
one, three titles containing "Firmware", shared tags, a bot-authored comment and an unshared subtree.
|
|
83
|
+
|
|
84
|
+
Row ids are UUID-shaped (`fd000000-0000-4000-8000-KKKKNNNNNNNN`, `KKKK` = kind, `NNNNNNNN` = sequence) so `format: uuid`
|
|
85
|
+
clients accept them; seeded rows use sequences below `0x1000`, created rows take the next value of `meta/counters`
|
|
86
|
+
(`next_id`, shared by rows, property ids and select-option ids). Ids are accepted dashed or as 32 hex characters and always
|
|
87
|
+
returned dashed.
|
|
88
|
+
|
|
89
|
+
## Operations
|
|
90
|
+
|
|
91
|
+
Every operation has one canonical shape: the **flattened Notion request** (path + query + body fields in one snake_case
|
|
92
|
+
object) in, and the **exact Notion response body** out. That is also the shape the Notion MCP server exposes per tool, so
|
|
93
|
+
the aliases carry the same schemas, and the HTTP codecs only move fields between the path/query and that object.
|
|
94
|
+
|
|
95
|
+
| operation | MCP alias | HTTP route |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `users.me` | `API-get-self` | `GET /v1/users/me` (served by the `get-user` route, see *Compatibility*) |
|
|
98
|
+
| `users.list` | `API-get-users` | `GET /v1/users` |
|
|
99
|
+
| `users.get` | `API-get-user` | `GET /v1/users/{user_id}` |
|
|
100
|
+
| `search` | `API-post-search` | `POST /v1/search` |
|
|
101
|
+
| `pages.create` | `API-post-page` | `POST /v1/pages` |
|
|
102
|
+
| `pages.retrieve` | `API-retrieve-a-page` | `GET /v1/pages/{page_id}` |
|
|
103
|
+
| `pages.update` | `API-patch-page` | `PATCH /v1/pages/{page_id}` |
|
|
104
|
+
| `pages.retrieve-property` | `API-retrieve-a-page-property` | `GET /v1/pages/{page_id}/properties/{property_id}` |
|
|
105
|
+
| `pages.move` | `API-move-page` | `POST /v1/pages/{page_id}/move` |
|
|
106
|
+
| `pages.retrieve-markdown` | `API-retrieve-page-markdown` | `GET /v1/pages/{page_id}/markdown` |
|
|
107
|
+
| `pages.update-markdown` | `API-update-page-markdown` | `PATCH /v1/pages/{page_id}/markdown` |
|
|
108
|
+
| `databases.create` | — | `POST /v1/databases` |
|
|
109
|
+
| `databases.retrieve` | `API-retrieve-a-database` | `GET /v1/databases/{database_id}` |
|
|
110
|
+
| `data-sources.retrieve` | `API-retrieve-a-data-source` | `GET /v1/data_sources/{data_source_id}` |
|
|
111
|
+
| `data-sources.query` | `API-query-data-source` | `POST /v1/data_sources/{data_source_id}/query` and legacy `POST /v1/databases/{database_id}/query` |
|
|
112
|
+
| `data-sources.update` | `API-update-a-data-source` | `PATCH /v1/data_sources/{data_source_id}` |
|
|
113
|
+
| `blocks.retrieve` | `API-retrieve-a-block` | `GET /v1/blocks/{block_id}` |
|
|
114
|
+
| `blocks.children.list` | `API-get-block-children` | `GET /v1/blocks/{block_id}/children` |
|
|
115
|
+
| `blocks.children.append` | `API-patch-block-children` | `PATCH /v1/blocks/{block_id}/children` |
|
|
116
|
+
| `blocks.update` | `API-update-a-block` | `PATCH /v1/blocks/{block_id}` |
|
|
117
|
+
| `blocks.delete` | `API-delete-a-block` | `DELETE /v1/blocks/{block_id}` (200 + block object, as Notion) |
|
|
118
|
+
| `comments.create` | `API-create-a-comment` | `POST /v1/comments` |
|
|
119
|
+
| `comments.list` | `API-retrieve-a-comment` | `GET /v1/comments?block_id=` |
|
|
120
|
+
| `workspace.context` | — | — (canonical only: workspace, acting user, resolved integration, `now` from virtual time, row bounds) |
|
|
121
|
+
| `workspace.trash` | — | — (canonical only: the trash roots — trashed pages and databases whose own parent is live — newest deletion first, cursor-paginated; backs the app's Trash panel, Notion has no public trash listing) |
|
|
122
|
+
|
|
123
|
+
Canonical names are also reachable as `POST /v1/operations/notion/<operation>` and as MCP tools `notion.<operation>`.
|
|
124
|
+
Mutations accept an optional `X-Firedrill-Idempotency-Key` request header (Notion clients never send one).
|
|
125
|
+
|
|
126
|
+
Behaviour highlights, all computed from state:
|
|
127
|
+
|
|
128
|
+
- **Property types**: `title`, `rich_text`, `number`, `select`, `multi_select`, `status`, `date`, `checkbox`, `url`,
|
|
129
|
+
`email`, `phone_number`, `people`, `relation` (single direction), and the computed `created_time`, `created_by`,
|
|
130
|
+
`last_edited_time`, `last_edited_by`. Values are validated against the data source's schema (`X is not a property that
|
|
131
|
+
exists.`, `Status is expected to be status.`, unknown status option, relation outside the related source, computed
|
|
132
|
+
property in a write → `400 validation_error`); unknown `select` / `multi_select` option names are added to the schema
|
|
133
|
+
as Notion does. `null` clears a value; a create fills omitted properties with empty values. Plain pages hold exactly
|
|
134
|
+
`title` (`properties: { title: [...] }` and `{ title: { title: [...] } }` are both accepted).
|
|
135
|
+
- **Query** (`data-sources.query`): Notion's per-type condition table (text `equals … is_not_empty`, number comparisons,
|
|
136
|
+
checkbox, select/status `equals`, multi-select/people/relation `contains`, date `before/after/on_or_before/on_or_after/
|
|
137
|
+
equals` and the relative windows `this_week`, `past_week/month/year`, `next_week/month/year` computed from virtual
|
|
138
|
+
time; date-only values compare by UTC calendar day, weeks start on Monday), `and`/`or` compounds nested at most two
|
|
139
|
+
levels with ≤ 100 conditions per group, `sorts` by property or `timestamp` applied stably (empties last, select/status
|
|
140
|
+
by option order), `filter_properties`, `in_trash` / `archived`, cursor pagination (`page_size` 1–100, `start_cursor` =
|
|
141
|
+
id of the last item; unknown → `The start_cursor provided is invalid.`). Anything else answers `400 validation_error`
|
|
142
|
+
with Notion's `body.filter…` wording.
|
|
143
|
+
- **Search**: case-insensitive substring match of every whitespace-separated token against page and data-source titles,
|
|
144
|
+
`filter.value: page | data_source`, `sort.timestamp: last_edited_time` ascending/descending (default descending),
|
|
145
|
+
cursor pagination; trashed and unshared objects are excluded.
|
|
146
|
+
- **Blocks**: `paragraph`, `heading_1/2/3`, `bulleted_list_item`, `numbered_list_item`, `to_do`, `toggle`, `quote`,
|
|
147
|
+
`callout`, `code`, `divider`, `bookmark`, `image` (external only) plus the maintained `child_page` / `child_database`
|
|
148
|
+
(their block id equals the page / database id). Appends take ≤ 100 children nested ≤ 2 levels, at the end or
|
|
149
|
+
`after` a sibling; updates replace `rich_text` wholly and merge `checked`, `color`, `language`, `icon`, `url`, `caption`
|
|
150
|
+
(the MCP server's `type: { <type>: … }` wrapper is accepted; a type change is rejected); deletes trash the subtree
|
|
151
|
+
(`archived: false` restores). Rich text supports `text` (annotations, link) and `mention` (`user`, `page`, `date`);
|
|
152
|
+
items are capped at 2000 characters (link URLs at 2000) and 100 per array, and one rich-text array may render to at most
|
|
153
|
+
800 000 UTF-8 bytes (the text is sent twice, as `text.content` and `plain_text`).
|
|
154
|
+
- **Trash**: `pages.update { in_trash: true }` (or `archived`) trashes the page, its blocks, child pages and child
|
|
155
|
+
databases; `in_trash: false` restores the subtree; editing a trashed object answers `Can't edit block that is archived…`.
|
|
156
|
+
`data-sources.update { in_trash: true }` trashes the source and its pages. Restore is a whole-subtree operation and
|
|
157
|
+
diverges from Notion here: blocks deleted individually with `blocks.delete` before the page was trashed come back
|
|
158
|
+
when the page is restored.
|
|
159
|
+
- **Move**: into a page (properties reduced to the title, a `child_page` block appears at the end of the target) or
|
|
160
|
+
into a data source (title kept under the target's title property, other values re-created empty); moving a page into
|
|
161
|
+
itself, a descendant or a trashed parent is rejected.
|
|
162
|
+
- **Markdown** (`pages.retrieve-markdown` / `pages.update-markdown`): a deterministic dialect — `#`/`##`/`###`,
|
|
163
|
+
paragraphs, `- ` bullets, `1. ` numbered items (renumbered), `- [ ] / - [x]` to-dos, `> ` quotes, `> <emoji> text`
|
|
164
|
+
callouts, fenced code with the Notion language name, `---`, `<details><summary>…</summary>` toggles, two-space
|
|
165
|
+
indentation for nested list children, inline `**bold**`, `*italic*`, `~~strike~~`, `` `code` ``, `[text](url)`,
|
|
166
|
+
user mentions as `[@Name](notion://user/<id>)`, child pages / databases as `[📄 Title](notion://page/<id>)` /
|
|
167
|
+
`[🗃️ Title](notion://database/<id>)` (kept in place, never duplicated, never removable through markdown), and
|
|
168
|
+
`bookmark` / `image` blocks as `<unknown type="…" id="…" url="…"/>` listed in `unknown_block_ids`. Nesting
|
|
169
|
+
(`<details>`, indented list children) follows the append rule — at most two levels below a top-level block, else the
|
|
170
|
+
append's `body.content[0]….children nests blocks deeper than 2 levels` error — and parsing is linear in the content
|
|
171
|
+
length (an unclosed `[`, `` ` ``, `**` or `~~` stays plain text). `replace_content`
|
|
172
|
+
needs `allow_deleting_content: true` when blocks would be removed; `update_content` applies `old_str` → `new_str`
|
|
173
|
+
(`replace_all_matches` for several hits; zero or ambiguous hits are rejected; `new_str` is literal text) and edits
|
|
174
|
+
unchanged blocks in place — at most 100 `content_updates` per request, and a step that would grow the markdown past
|
|
175
|
+
`max_markdown_bytes` is rejected before it is built (`the resulting markdown exceeds 102400 bytes (at
|
|
176
|
+
body.content_updates[i])`);
|
|
177
|
+
`insert_content` adds at `position.type: start | end`. `replace_content_range` and `allow_async` are rejected.
|
|
178
|
+
- **Comments**: `comments.create` with `parent.page_id` starts a discussion, with `discussion_id` joins one;
|
|
179
|
+
`comments.list` lists a page's or block's comments in creation order, paginated.
|
|
180
|
+
- **Bounds**: every scan is bounded by `meta/limits.max_rows_per_namespace` (default 10 000) and fails with
|
|
181
|
+
`FAILED_PRECONDITION` (HTTP 400 `validation_error`, "state exceeds the supported bound of N rows in <namespace>")
|
|
182
|
+
instead of truncating; `max_page_size` 100, `max_children_per_append` 100, `max_markdown_bytes` 102 400 (UTF-8 bytes), 1000 block
|
|
183
|
+
elements per parsed markdown document, 100 `content_updates` per request. Block positions are gapless among a
|
|
184
|
+
parent's live children; trashed blocks keep their last position (a restored block returns near it). Lower the
|
|
185
|
+
`limits` row in a scenario to prove the failure (the shipped `bounded` scenario uses 4).
|
|
186
|
+
|
|
187
|
+
### Errors
|
|
188
|
+
|
|
189
|
+
Tool errors are rendered with Notion's envelope `{ "object": "error", "status", "code", "message", "request_id" }`:
|
|
190
|
+
`VALIDATION_ERROR` → 400 `validation_error`, `FAILED_PRECONDITION` → 400 `validation_error`, `UNAUTHORIZED` → 401
|
|
191
|
+
`unauthorized`, `RESTRICTED_RESOURCE` → 403 `restricted_resource`, `OBJECT_NOT_FOUND` → 404 `object_not_found`,
|
|
192
|
+
`CONFLICT_ERROR` → 409 `conflict_error`, `RATE_LIMITED` → 429 `rate_limited` (+ `Retry-After: 1`), `SERVICE_UNAVAILABLE` →
|
|
193
|
+
503 `service_unavailable`. Messages follow Notion's wording where known (`Could not find page with ID: … Make sure the
|
|
194
|
+
relevant pages and databases are shared with your integration.`, `path.page_id should be a valid uuid`, `query failed
|
|
195
|
+
validation: query.page_size should be ≤ 100, instead was 250.`). Every validation message names the section the value
|
|
196
|
+
came from, so a body value is reported as `body.parent.page_id`, a query value as `query.block_id` and a path segment
|
|
197
|
+
as `path.page_id`. Framework outcomes keep their statuses — schema-invalid
|
|
198
|
+
input 400, missing grant 403, unknown route 404/405 — but the bodies of the declared routes are Notion-shaped too.
|
|
199
|
+
|
|
200
|
+
Query `page_size` on `GET /v1/users`, `GET /v1/blocks/{block_id}/children`, `GET /v1/comments` and
|
|
201
|
+
`GET /v1/pages/{page_id}/properties/{property_id}` is never rejected by the route codec: a value that is not a plain
|
|
202
|
+
decimal integer (`abc`, empty, `1e999`, `1.5`) answers 400 `validation_error` `query failed validation: query.page_size
|
|
203
|
+
should be a number, instead was \`"abc"\`.`, a non-integer number answers `should be an integer`, a value below the range
|
|
204
|
+
(`0`, `-1`) answers `should be ≥ 1, instead was 0.` and one above it (`101`, `99999999999`) `should be ≤ 100`. The same
|
|
205
|
+
messages are used for `page_size` in a JSON body (`POST /v1/search`, data source and database query, canonical
|
|
206
|
+
`workspace.trash`), where the field is named `body.page_size` and any JSON type reaches the handler, so
|
|
207
|
+
`{"page_size":"abc"}` answers Notion's message rather than a schema rejection. Non-string values in validation messages
|
|
208
|
+
are printed as JSON (`instead was \`{}\``).
|
|
209
|
+
|
|
210
|
+
`property_id` path segments are decoded once by the server. A property matches when its id or name equals that value,
|
|
211
|
+
when its (URL-encoded, Notion-style) id decodes to it, or when the value itself decodes once more to an id or name — so
|
|
212
|
+
both `/properties/%3AUPp` and a client-side re-encoded `/properties/%253AUPp` resolve an id stored as `%3AUPp`, and
|
|
213
|
+
percent escapes are compared case-insensitively, so a client that re-encodes with lower-case hexadecimal (`%3a…`)
|
|
214
|
+
resolves the same property. A
|
|
215
|
+
malformed leftover encoding (`%25`, `%25E0%25A4%25A`, `%25ZZ`) never throws; it answers 400 `validation_error`
|
|
216
|
+
`Could not find property with name or id: %`. Error messages that echo caller input (ids, property keys) are capped at the
|
|
217
|
+
framework's 1000-character outcome-message limit and end with `…` when cut.
|
|
218
|
+
|
|
219
|
+
## Events and faults
|
|
220
|
+
|
|
221
|
+
| id | kind | when |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `page.created` | event | `pages.create` (`entity_id`, `parent_type`, `parent_id`, `database_id`, `author_id`) |
|
|
224
|
+
| `page.properties_updated` | event | `pages.update` with properties/icon/cover (`updated_properties` = property ids, `icon`, `cover`); `pages.move` (`updated_properties: []`) |
|
|
225
|
+
| `page.content_updated` | event | `pages.create` with `children`, `blocks.children.append`, `blocks.update`, `blocks.delete`, `pages.update-markdown` (`updated_blocks`) |
|
|
226
|
+
| `page.deleted` / `page.undeleted` | event | `pages.update` trashing / restoring a live / trashed page |
|
|
227
|
+
| `comment.created` | event | `comments.create` (`discussion_id`, `parent_type`, `parent_id`, `page_id`) |
|
|
228
|
+
| `rate-limited` | fault (`before`) | `search`, `data-sources.query`, `blocks.children.list`, `pages.retrieve` answer 429 `rate_limited` with `Retry-After: 1` |
|
|
229
|
+
| `write-unavailable` | fault (`before`) | `pages.create`, `pages.update`, `blocks.children.append`, `comments.create` answer 503 `service_unavailable`; nothing is written |
|
|
230
|
+
| `update-committed-response-lost` | fault (`after_commit`) | `pages.update` commits but the caller sees 409 `conflict_error` — models Notion's transient save conflict where a retry must re-read first |
|
|
231
|
+
|
|
232
|
+
Scenarios `rate-limited`, `write-unavailable`, `update-lost` and `bounded` ship in `firedrill/`; no subscriptions or
|
|
233
|
+
callbacks are declared (attach your own callbacks to the events above).
|
|
234
|
+
|
|
235
|
+
## App
|
|
236
|
+
|
|
237
|
+
`firedrill serve` also serves a browser app (declared as `ui: { root: "app/site" }`) that recreates the Notion web
|
|
238
|
+
client a workspace member sees, with every control wired to the operations above through `/_firedrill/client.js`:
|
|
239
|
+
|
|
240
|
+
- **Sidebar**: workspace switcher (workspace icon and name; the menu shows the acting person or bot and the integration's
|
|
241
|
+
capabilities), Search (`⌘/Ctrl K`), Home, Inbox (every comment of the 25 most recently edited live pages, read with
|
|
242
|
+
`start_cursor`; it says *Showing comments from the 25 most recently edited pages.* when more exist), the *Private* page tree (roots from `search`, children loaded on expand
|
|
243
|
+
from `blocks.children.list` in block order, full-page databases included, inline databases and database rows excluded),
|
|
244
|
+
Settings / Templates / Trash / Invite members panels (lists the app loads with a request cap — the page index, a page's
|
|
245
|
+
subpages, workspace members, a page's comments — show a notice when the cap trips instead of hiding rows), `+` to create pages (`pages.create`; integrations cannot create at
|
|
246
|
+
the workspace root, so new top-level pages go under the first root page and the app says so).
|
|
247
|
+
- **Home**: greeting from virtual time, *Upcoming events* (not simulated: no calendar connection), *Recently visited* cards (index sorted by `last_edited_time` — the API has no visit
|
|
248
|
+
history) and a *Home database view* widget: the first data source with a people + status property, queried with
|
|
249
|
+
`people contains <acting user>`.
|
|
250
|
+
- **Page**: icon (emoji picker → `pages.update icon`), title (`pages.update`), property panel for database rows with the
|
|
251
|
+
right editor per type (text/number/url/email/phone inputs, date with optional end date, select/status/multi-select
|
|
252
|
+
option pickers that can create options, people picker from `users.list`, relation picker backed by
|
|
253
|
+
`data-sources.query`, checkbox), page comments under the title (`comments.list`, reply / new discussion via
|
|
254
|
+
`comments.create`), the block editor (`blocks.children.list` paginated with *Load more*; plain-text editing of text
|
|
255
|
+
blocks on blur → `blocks.update`, Enter appends a block after the current one, Backspace on an empty block deletes it,
|
|
256
|
+
`/` opens the block menu → `blocks.children.append` with `after`; to-do checkboxes; toggles load children on expand;
|
|
257
|
+
the `⋮⋮` handle menu offers Comment and Delete (confirmed) → `blocks.delete`; child pages / inline databases render in
|
|
258
|
+
place), the top-bar comments panel, and the `…` menu with Copy link, Markdown (`pages.retrieve-markdown` with a
|
|
259
|
+
*Replace page content* editor → `pages.update-markdown replace_content`, confirmed), Move to (`pages.move`) and Move
|
|
260
|
+
to Trash (confirmed, with Undo). Trashed pages show the red banner with *Restore page*.
|
|
261
|
+
- **Database** (full page and inline): Table and Board views, *Filter* builder composing real `data-sources.query`
|
|
262
|
+
filters (per-type conditions, `and`), *Sort* menu, search-in-view (title `contains`), *New* (`pages.create` in the
|
|
263
|
+
data source, then the side peek), inline cell editing (`pages.update`), *Load 50 more* pagination, `COUNT` footer,
|
|
264
|
+
column menu (sort, filter, rename, delete) and the *Properties* editor (`data-sources.update`: add, rename, delete).
|
|
265
|
+
Board groups by the first status (else select) property client-side over the same query.
|
|
266
|
+
- **States**: skeleton loading, empty page / *No pages yet* / *No results* / empty trash and inbox, the product's
|
|
267
|
+
"This content doesn't exist or you don't have access." page for `object_not_found`, capability and framework denials
|
|
268
|
+
as toasts or the *You don't have access* page, a rate-limit retry state, a connection screen when the app is opened
|
|
269
|
+
outside its local link; every timestamp and "today" comes from `workspace.context.now`, never the browser clock; the
|
|
270
|
+
page re-fetches when `getContext().revision` changes unless the viewer is typing; mutations carry idempotency keys and
|
|
271
|
+
destructive actions confirm. Usable at phone width (the sidebar becomes a drawer).
|
|
272
|
+
|
|
273
|
+
The app uses the same records as the API: a page created over REST appears in the sidebar within two seconds; a cell edited
|
|
274
|
+
in the table is visible to `pages.retrieve`. External images and bookmarks are never fetched (the serving policy blocks
|
|
275
|
+
remote requests), so image blocks and covers render as frames with the URL as caption. Assets: the official Notion logo
|
|
276
|
+
files under `firedrill/tools/notion/app/assets/` (sources in `ATTRIBUTION.md`) and the Inter typeface (SIL OFL).
|
|
277
|
+
|
|
278
|
+
## Conformance
|
|
279
|
+
|
|
280
|
+
`firedrill tool test notion` runs 16 drills (`firedrill/*.drill.json`, suite `conformance`) with a Node-built-ins-only
|
|
281
|
+
target (`test/conformance.mjs`, bindings `http` + `mcp`) that exercises every operation, every declared error, every
|
|
282
|
+
event and every fault, then re-runs the suite to prove determinism: workspace reads with pagination and validation
|
|
283
|
+
errors, task triage with real filters/sorts/create/update/move/comments, page authoring with blocks, markdown and a new
|
|
284
|
+
database, trash and restore, the MCP aliases, four identity/scope drills, the framework-denied actor, and one drill per
|
|
285
|
+
fault plus the bounded scenario, and a byte-budget drill (oversized rich text refused with nothing stored; near-limit
|
|
286
|
+
blocks, comments, search results and query rows read back once each across byte-filled pages under 1 MiB; oversized
|
|
287
|
+
markdown and database writes answer `validation_error`), and a schema-growth drill (a data source schema is refused at
|
|
288
|
+
exactly one byte over what it may add to every page, accepted at the limit, and its rows stay readable).
|
|
289
|
+
|
|
290
|
+
## Compatibility
|
|
291
|
+
|
|
292
|
+
- **Not verified against a real client.** `manifest.compatibility` is empty: neither `@notionhq/client` nor
|
|
293
|
+
`@notionhq/notion-mcp-server` has been run against `firedrill serve` yet. The MCP aliases reproduce the tool names and
|
|
294
|
+
flattened argument shapes of the open-source Notion MCP server 2.x (`API-` + OpenAPI operationId) as read from its
|
|
295
|
+
tool schemas; the hosted `mcp.notion.com` contract (`notion-search`, `notion-fetch`, …) is a different, undocumented
|
|
296
|
+
surface and is **not** claimed.
|
|
297
|
+
- `GET /v1/users/me` is served by the `get-user` route (`users.get` with `user_id: "me"`, no user-information capability
|
|
298
|
+
needed) because the framework rejects a literal path segment next to the `/v1/users/{user_id}` template. The canonical
|
|
299
|
+
`users.me` operation and its `API-get-self` alias are unaffected; in drill evidence a `GET /v1/users/me` counts as a
|
|
300
|
+
`users.get` call.
|
|
301
|
+
- `Notion-Version` is accepted but neither enforced nor echoed; responses always use the 2025-09-03 shapes (database
|
|
302
|
+
containers without `properties`, `parent.type: data_source_id` on database pages, `type: page_or_data_source` lists)
|
|
303
|
+
plus the 2026-03-11 markdown endpoints. A 2022-06-28 client works only through the legacy `POST /v1/databases/{id}/query`
|
|
304
|
+
and `pages.create` with `parent.database_id`. `missing_version` is never returned.
|
|
305
|
+
- Unsupported Notion paths (`/v1/oauth/*`, `/v1/file_uploads*`, `POST /v1/data_sources`, `PATCH /v1/databases/{id}`,
|
|
306
|
+
data-source templates, …) do not exist: the framework answers 404 (or 405 when only other methods exist on the path).
|
|
307
|
+
Never a plausible stub.
|
|
308
|
+
|
|
309
|
+
## Limitations
|
|
310
|
+
|
|
311
|
+
- Single workspace, single-token model: no OAuth, no public integrations, no multi-workspace tokens; `users.me` always
|
|
312
|
+
returns a bot owned by the workspace.
|
|
313
|
+
- Property types limited to the list above — no formulas, rollups, files, unique ids, verification, buttons or places;
|
|
314
|
+
relations are single-direction (no synced property); `people` values must be workspace users; `date.time_zone` is
|
|
315
|
+
stored, not interpreted.
|
|
316
|
+
- Block types limited to the list above — `table`, columns, synced blocks, embeds, media uploads, `link_to_page` and
|
|
317
|
+
direct `child_page` / `child_database` appends are rejected with `validation_error`; `after` is supported but the
|
|
318
|
+
2026-03-11 `position` object for appends is not.
|
|
319
|
+
- The markdown dialect is a deterministic subset, not Notion's enhanced markdown: text containing markdown markers does
|
|
320
|
+
not escape, date mentions render as plain text, `replace_content_range`, `allow_async` (202 tasks) and `include_transcript`
|
|
321
|
+
are not supported, and `update_content` keeps block ids only where the old and new markdown align block by block.
|
|
322
|
+
- Search matches titles only (no content search, fuzziness or relevance ranking); cursors are last-item ids valid only
|
|
323
|
+
against unchanged ordering; there is no `request_status.incomplete`, no `partial` objects, no `public_url`.
|
|
324
|
+
- No permanent deletion (Notion has none), no comment edits/deletes/attachments/`display_name` overrides, no
|
|
325
|
+
data-source schema events, no webhooks, no optimistic concurrency (`conflict_error` only occurs through the fault),
|
|
326
|
+
no `Retry-After` beyond the fault header.
|
|
327
|
+
- Framework-level `invalid` (schema) and `denied` outcomes use the framework's statuses; only their bodies are
|
|
328
|
+
Notion-shaped. `FAILED_PRECONDITION` (row bound) has no Notion equivalent and is sent as 400 `validation_error`.
|
|
329
|
+
- Response size: the framework refuses any response over 1 MiB, so every list (`users.list`, `search`, data source
|
|
330
|
+
query, block children, comments, property items, trash) ends a page at `page_size` items or about 900 KB of encoded
|
|
331
|
+
results, whichever comes first, with `next_cursor` naming the last item returned. Writes are bounded so one object
|
|
332
|
+
always fits one response: a block's content, a page, a comment, a database and a data source may render to at most
|
|
333
|
+
800 000 UTF-8 bytes, a data source schema to 150 000 (and to at most 150 000 bytes added to the rendering of each of
|
|
334
|
+
its pages, counting `created_by`/`last_edited_by` as the widest expanded user, so a stored page still fits after any
|
|
335
|
+
later schema change), and one create/append request may add 900 000 bytes of block
|
|
336
|
+
content; beyond that the write answers `validation_error` and stores nothing. A read whose single response would
|
|
337
|
+
still pass 1 000 000 bytes (a page's markdown over many large blocks, for example) answers `validation_error`
|
|
338
|
+
`The response is too large: …`; read such a page through `GET /v1/blocks/{id}/children` instead. The request body
|
|
339
|
+
itself is capped at 1 MiB by the framework (HTTP 413).
|
|
340
|
+
- JSON request bodies may nest at most 512 levels: a deeper body is refused by the route codec with `400`
|
|
341
|
+
`framework.HTTP_REQUEST_MAPPING_FAILED` ("JSON nesting is deeper than the maximum depth of 512") before argument
|
|
342
|
+
validation, because the framework validates arguments recursively and a deeper body would overflow the stack.
|
|
343
|
+
Depth is measured iteratively, and ordinary bodies (blocks, rich text, filters) are far below the bound. The bound
|
|
344
|
+
is applied by the provider-shaped `/v1/...` routes; the canonical `POST /v1/operations/notion/<operation>` path is
|
|
345
|
+
decoded by the framework itself, where a body nested a few thousand levels deep still answers `413`
|
|
346
|
+
`{"error":"Maximum call stack size exceeded"}`; the exact depth depends on the serve process's stack limit and
|
|
347
|
+
has been measured anywhere from about 3,000 to more than 6,000 levels (framework limitation, recorded in
|
|
348
|
+
`specs/notion/VERIFICATION.md`).
|
|
349
|
+
- Malformed percent-encoding is decoded leniently by the framework: a bad escape such as `%E0%A4%A` becomes U+FFFD
|
|
350
|
+
(`%ZZ` is not a valid escape and stays literal in query strings and bodies — e.g. `filter_properties=%ZZ` reaches the
|
|
351
|
+
handler and answers Notion's own "Could not find property with name or id: %ZZ" — while any unresolvable escape,
|
|
352
|
+
`%ZZ` included, in a *path* segment is a `404` "route not found"). A query-language input carrying U+FFFD is
|
|
353
|
+
treated as a mangled request rather than a literal to match, and answers `validation_error` "… contains an invalid
|
|
354
|
+
character (U+FFFD); check the request's percent-encoding": `search` `query`, data-source query `filter` property
|
|
355
|
+
names and string condition values, `sorts[].property`, and `filter_properties`. Correctly encoded non-ASCII search
|
|
356
|
+
terms (accents, CJK, emoji) are unaffected.
|
|
357
|
+
- `GET /v1/comments` without `block_id` is refused by the declared input schema (`block_id` is required, as in
|
|
358
|
+
Notion), so the message is Notion's envelope carrying the schema text "arguments do not match notion.comments.list"
|
|
359
|
+
rather than a hand-written field message.
|
|
360
|
+
- Bounds: 10 000 rows per namespace (configurable through `meta/limits`), 100 items per page, 100 children per append
|
|
361
|
+
nested two levels, 2000 characters per rich-text item, 100 KB of markdown, 1000 block elements per parsed markdown
|
|
362
|
+
document (Notion's per-request block limit; `update_content` counts both the current page and the result, so pages
|
|
363
|
+
over 1000 blocks are edited through the blocks API), 64 stored block levels below a page (appends that would go
|
|
364
|
+
deeper fail with `validation_error`). Each bound answers `validation_error`; nothing is truncated. `__proto__` is
|
|
365
|
+
reserved as a property name, but the two forms differ: *renaming* a property to `__proto__`
|
|
366
|
+
(`data-sources.update { properties: { Highlights: { name: "__proto__" } } }`) is refused with `validation_error`
|
|
367
|
+
"… uses the reserved property name __proto__.", while a request that carries `__proto__` as a property *key* never
|
|
368
|
+
reaches the Tool — the framework strips own `__proto__` keys while decoding the body, so the call succeeds (`200`)
|
|
369
|
+
with that one field silently absent from the created or updated schema (see `specs/notion/VERIFICATION.md`).
|
|
370
|
+
- App: editing a text block replaces its rich text with one plain run (annotations, links and mentions of the edited
|
|
371
|
+
block are flattened; untouched blocks keep them); block types cannot be changed in place (the `/` menu inserts a new
|
|
372
|
+
block); the sidebar lists the first 5 000 visible pages; *Recently visited* is ordered by last edit; favorites,
|
|
373
|
+
expanded tree nodes and view filters are per-browser conveniences, not workspace state; Notion AI, teamspaces,
|
|
374
|
+
templates, sharing settings, notifications and permanent deletion have no API surface and are shown as informational
|
|
375
|
+
panels rather than working controls.
|
|
376
|
+
- App controls outside this Tool's scope render in place with the product's glyph, hover state and tooltip and open a
|
|
377
|
+
short *Not simulated by this Tool* panel (or are disabled): sidebar *Meetings*, *Notion AI* and *Marketplace*; the
|
|
378
|
+
top-bar *View all updates* (clock), *Add to Favorites* (star) and, on full-page databases, *Comments* and *Share*;
|
|
379
|
+
the database *Automations* (lightning) button, the *New* caret's *New template*, and the Timeline, Calendar, List,
|
|
380
|
+
Gallery, Chart, Feed and Map layouts in the *Add a new view* menu (Table and Board work); Home *Upcoming events*
|
|
381
|
+
(*Connect calendar*) and the Home *Learn* cards (text-only guide cards; the guides live on the vendor's help
|
|
382
|
+
center); the sidebar *Teamspaces* section (heading, hover *...* and *+*, *Browse teamspaces*) and *Shared* section
|
|
383
|
+
(heading, hover *+*, *Start collaborating*), drawn in place without invented entries because the API does not model
|
|
384
|
+
teamspaces or per-member sharing; the cover *Reposition* button and the cover picker's *Upload* and *Unsplash* tabs
|
|
385
|
+
(visibly disabled); and, in the inline text toolbar shown over a selection, *Ask AI*, *Turn into* (*Text*), *Link*,
|
|
386
|
+
*Bold*, *Italicize*, *Underline*, *Strike-through*, *Mark as code*, *Text color* and *...* (edited blocks are
|
|
387
|
+
written as one plain rich-text run). Working: *Add icon*, *Add cover* and *Change cover* (Gallery gradients, Link
|
|
388
|
+
and Remove through `pages.update` `cover`; external images are drawn as gradients), *Add comment*, and the toolbar's
|
|
389
|
+
*Comment* (opens the block's discussions).
|
|
390
|
+
|
|
391
|
+
## Trademarks
|
|
392
|
+
|
|
393
|
+
Notion and the Notion logo are trademarks of Notion Labs, Inc. They are used here only to identify the simulated
|
|
394
|
+
service in a test environment; this package is an independent Firedrill Tool and is not affiliated with, sponsored by
|
|
395
|
+
or endorsed by Notion Labs, Inc. The logo files under `firedrill/tools/notion/app/` (`notion.svg`, `notion-wordmark.svg`)
|
|
396
|
+
are unmodified downloads whose sources are recorded in `firedrill/tools/notion/app/assets/ATTRIBUTION.md`, together with
|
|
397
|
+
the Inter font's SIL Open Font License.
|
|
398
|
+
|
|
399
|
+
## Safety
|
|
400
|
+
|
|
401
|
+
Tools and conformance targets are trusted local executable code, not a sandbox. Review before running. Keep
|
|
402
|
+
credentials and generated worlds/reports out of the package and repository; the file list excludes `.firedrill/`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"target": {
|
|
4
|
+
"id": "conformance-agent",
|
|
5
|
+
"kind": "command",
|
|
6
|
+
"bindings": [
|
|
7
|
+
"http",
|
|
8
|
+
"mcp"
|
|
9
|
+
],
|
|
10
|
+
"executable": "node",
|
|
11
|
+
"arguments": [
|
|
12
|
+
"test/conformance.mjs"
|
|
13
|
+
],
|
|
14
|
+
"workingDirectory": ".",
|
|
15
|
+
"timeoutMs": 90000
|
|
16
|
+
}
|
|
17
|
+
}
|