@open-everywhere/provider-notion 0.1.0

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 (186) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +48 -0
  4. package/agent/channels/notion-acting-identity.ts +88 -0
  5. package/agent/channels/notion-ancestry-trust.ts +458 -0
  6. package/agent/channels/notion-api.ts +1290 -0
  7. package/agent/channels/notion-bot-user.ts +250 -0
  8. package/agent/channels/notion-catch-up.ts +339 -0
  9. package/agent/channels/notion-credentials.ts +417 -0
  10. package/agent/channels/notion-engagement.ts +372 -0
  11. package/agent/channels/notion-event-normalizer.ts +587 -0
  12. package/agent/channels/notion-identity.ts +271 -0
  13. package/agent/channels/notion-ingress-drain.ts +289 -0
  14. package/agent/channels/notion-ingress.ts +182 -0
  15. package/agent/channels/notion-message-ops.ts +140 -0
  16. package/agent/channels/notion-origin-codec.ts +38 -0
  17. package/agent/channels/notion-post-arm.ts +189 -0
  18. package/agent/channels/notion-question-return.ts +223 -0
  19. package/agent/channels/notion-read.ts +283 -0
  20. package/agent/channels/notion-scope.ts +560 -0
  21. package/agent/channels/notion-stop.ts +169 -0
  22. package/agent/channels/notion-supervisor.ts +856 -0
  23. package/agent/channels/notion-webhook-timing.ts +131 -0
  24. package/agent/channels/notion-webhook-verifier.ts +55 -0
  25. package/agent/channels/notion-webhook.ts +963 -0
  26. package/agent/channels/notion-work-search.ts +74 -0
  27. package/agent/channels/notion-work.ts +144 -0
  28. package/agent/channels/notion.ts +1242 -0
  29. package/agent/contributions.ts +39 -0
  30. package/agent/docs-generated.ts +15 -0
  31. package/agent/memory/blocks.ts +295 -0
  32. package/agent/memory/memory.ts +622 -0
  33. package/agent/provider-events.ts +198 -0
  34. package/agent/register.ts +144 -0
  35. package/agent/schedules/notion-ingress.ts +20 -0
  36. package/agent/surface-parity.ts +105 -0
  37. package/agent/tools/notion_search.ts +110 -0
  38. package/agent/tools/read_notion_comments.ts +250 -0
  39. package/agent/tools/read_notion_page.ts +351 -0
  40. package/dist/agent/channels/notion-acting-identity.d.ts +23 -0
  41. package/dist/agent/channels/notion-acting-identity.d.ts.map +1 -0
  42. package/dist/agent/channels/notion-acting-identity.js +85 -0
  43. package/dist/agent/channels/notion-acting-identity.js.map +1 -0
  44. package/dist/agent/channels/notion-ancestry-trust.d.ts +208 -0
  45. package/dist/agent/channels/notion-ancestry-trust.d.ts.map +1 -0
  46. package/dist/agent/channels/notion-ancestry-trust.js +331 -0
  47. package/dist/agent/channels/notion-ancestry-trust.js.map +1 -0
  48. package/dist/agent/channels/notion-api.d.ts +655 -0
  49. package/dist/agent/channels/notion-api.d.ts.map +1 -0
  50. package/dist/agent/channels/notion-api.js +938 -0
  51. package/dist/agent/channels/notion-api.js.map +1 -0
  52. package/dist/agent/channels/notion-bot-user.d.ts +107 -0
  53. package/dist/agent/channels/notion-bot-user.d.ts.map +1 -0
  54. package/dist/agent/channels/notion-bot-user.js +192 -0
  55. package/dist/agent/channels/notion-bot-user.js.map +1 -0
  56. package/dist/agent/channels/notion-catch-up.d.ts +64 -0
  57. package/dist/agent/channels/notion-catch-up.d.ts.map +1 -0
  58. package/dist/agent/channels/notion-catch-up.js +289 -0
  59. package/dist/agent/channels/notion-catch-up.js.map +1 -0
  60. package/dist/agent/channels/notion-credentials.d.ts +210 -0
  61. package/dist/agent/channels/notion-credentials.d.ts.map +1 -0
  62. package/dist/agent/channels/notion-credentials.js +360 -0
  63. package/dist/agent/channels/notion-credentials.js.map +1 -0
  64. package/dist/agent/channels/notion-engagement.d.ts +219 -0
  65. package/dist/agent/channels/notion-engagement.d.ts.map +1 -0
  66. package/dist/agent/channels/notion-engagement.js +283 -0
  67. package/dist/agent/channels/notion-engagement.js.map +1 -0
  68. package/dist/agent/channels/notion-event-normalizer.d.ts +194 -0
  69. package/dist/agent/channels/notion-event-normalizer.d.ts.map +1 -0
  70. package/dist/agent/channels/notion-event-normalizer.js +542 -0
  71. package/dist/agent/channels/notion-event-normalizer.js.map +1 -0
  72. package/dist/agent/channels/notion-identity.d.ts +119 -0
  73. package/dist/agent/channels/notion-identity.d.ts.map +1 -0
  74. package/dist/agent/channels/notion-identity.js +205 -0
  75. package/dist/agent/channels/notion-identity.js.map +1 -0
  76. package/dist/agent/channels/notion-ingress-drain.d.ts +66 -0
  77. package/dist/agent/channels/notion-ingress-drain.d.ts.map +1 -0
  78. package/dist/agent/channels/notion-ingress-drain.js +185 -0
  79. package/dist/agent/channels/notion-ingress-drain.js.map +1 -0
  80. package/dist/agent/channels/notion-ingress.d.ts +64 -0
  81. package/dist/agent/channels/notion-ingress.d.ts.map +1 -0
  82. package/dist/agent/channels/notion-ingress.js +130 -0
  83. package/dist/agent/channels/notion-ingress.js.map +1 -0
  84. package/dist/agent/channels/notion-message-ops.d.ts +3 -0
  85. package/dist/agent/channels/notion-message-ops.d.ts.map +1 -0
  86. package/dist/agent/channels/notion-message-ops.js +126 -0
  87. package/dist/agent/channels/notion-message-ops.js.map +1 -0
  88. package/dist/agent/channels/notion-origin-codec.d.ts +7 -0
  89. package/dist/agent/channels/notion-origin-codec.d.ts.map +1 -0
  90. package/dist/agent/channels/notion-origin-codec.js +32 -0
  91. package/dist/agent/channels/notion-origin-codec.js.map +1 -0
  92. package/dist/agent/channels/notion-post-arm.d.ts +3 -0
  93. package/dist/agent/channels/notion-post-arm.d.ts.map +1 -0
  94. package/dist/agent/channels/notion-post-arm.js +185 -0
  95. package/dist/agent/channels/notion-post-arm.js.map +1 -0
  96. package/dist/agent/channels/notion-question-return.d.ts +77 -0
  97. package/dist/agent/channels/notion-question-return.d.ts.map +1 -0
  98. package/dist/agent/channels/notion-question-return.js +183 -0
  99. package/dist/agent/channels/notion-question-return.js.map +1 -0
  100. package/dist/agent/channels/notion-read.d.ts +62 -0
  101. package/dist/agent/channels/notion-read.d.ts.map +1 -0
  102. package/dist/agent/channels/notion-read.js +257 -0
  103. package/dist/agent/channels/notion-read.js.map +1 -0
  104. package/dist/agent/channels/notion-scope.d.ts +188 -0
  105. package/dist/agent/channels/notion-scope.d.ts.map +1 -0
  106. package/dist/agent/channels/notion-scope.js +433 -0
  107. package/dist/agent/channels/notion-scope.js.map +1 -0
  108. package/dist/agent/channels/notion-stop.d.ts +48 -0
  109. package/dist/agent/channels/notion-stop.d.ts.map +1 -0
  110. package/dist/agent/channels/notion-stop.js +141 -0
  111. package/dist/agent/channels/notion-stop.js.map +1 -0
  112. package/dist/agent/channels/notion-supervisor.d.ts +462 -0
  113. package/dist/agent/channels/notion-supervisor.d.ts.map +1 -0
  114. package/dist/agent/channels/notion-supervisor.js +482 -0
  115. package/dist/agent/channels/notion-supervisor.js.map +1 -0
  116. package/dist/agent/channels/notion-webhook-timing.d.ts +35 -0
  117. package/dist/agent/channels/notion-webhook-timing.d.ts.map +1 -0
  118. package/dist/agent/channels/notion-webhook-timing.js +99 -0
  119. package/dist/agent/channels/notion-webhook-timing.js.map +1 -0
  120. package/dist/agent/channels/notion-webhook-verifier.d.ts +5 -0
  121. package/dist/agent/channels/notion-webhook-verifier.d.ts.map +1 -0
  122. package/dist/agent/channels/notion-webhook-verifier.js +53 -0
  123. package/dist/agent/channels/notion-webhook-verifier.js.map +1 -0
  124. package/dist/agent/channels/notion-webhook.d.ts +242 -0
  125. package/dist/agent/channels/notion-webhook.d.ts.map +1 -0
  126. package/dist/agent/channels/notion-webhook.js +675 -0
  127. package/dist/agent/channels/notion-webhook.js.map +1 -0
  128. package/dist/agent/channels/notion-work-search.d.ts +9 -0
  129. package/dist/agent/channels/notion-work-search.d.ts.map +1 -0
  130. package/dist/agent/channels/notion-work-search.js +69 -0
  131. package/dist/agent/channels/notion-work-search.js.map +1 -0
  132. package/dist/agent/channels/notion-work.d.ts +68 -0
  133. package/dist/agent/channels/notion-work.d.ts.map +1 -0
  134. package/dist/agent/channels/notion-work.js +126 -0
  135. package/dist/agent/channels/notion-work.js.map +1 -0
  136. package/dist/agent/channels/notion.d.ts +95 -0
  137. package/dist/agent/channels/notion.d.ts.map +1 -0
  138. package/dist/agent/channels/notion.js +1081 -0
  139. package/dist/agent/channels/notion.js.map +1 -0
  140. package/dist/agent/contributions.d.ts +32 -0
  141. package/dist/agent/contributions.d.ts.map +1 -0
  142. package/dist/agent/contributions.js +37 -0
  143. package/dist/agent/contributions.js.map +1 -0
  144. package/dist/agent/docs-generated.d.ts +3 -0
  145. package/dist/agent/docs-generated.d.ts.map +1 -0
  146. package/dist/agent/docs-generated.js +13 -0
  147. package/dist/agent/docs-generated.js.map +1 -0
  148. package/dist/agent/memory/blocks.d.ts +58 -0
  149. package/dist/agent/memory/blocks.d.ts.map +1 -0
  150. package/dist/agent/memory/blocks.js +264 -0
  151. package/dist/agent/memory/blocks.js.map +1 -0
  152. package/dist/agent/memory/memory.d.ts +157 -0
  153. package/dist/agent/memory/memory.d.ts.map +1 -0
  154. package/dist/agent/memory/memory.js +471 -0
  155. package/dist/agent/memory/memory.js.map +1 -0
  156. package/dist/agent/provider-events.d.ts +3 -0
  157. package/dist/agent/provider-events.d.ts.map +1 -0
  158. package/dist/agent/provider-events.js +196 -0
  159. package/dist/agent/provider-events.js.map +1 -0
  160. package/dist/agent/register.d.ts +5 -0
  161. package/dist/agent/register.d.ts.map +1 -0
  162. package/dist/agent/register.js +128 -0
  163. package/dist/agent/register.js.map +1 -0
  164. package/dist/agent/schedules/notion-ingress.d.ts +6 -0
  165. package/dist/agent/schedules/notion-ingress.d.ts.map +1 -0
  166. package/dist/agent/schedules/notion-ingress.js +16 -0
  167. package/dist/agent/schedules/notion-ingress.js.map +1 -0
  168. package/dist/agent/surface-parity.d.ts +3 -0
  169. package/dist/agent/surface-parity.d.ts.map +1 -0
  170. package/dist/agent/surface-parity.js +99 -0
  171. package/dist/agent/surface-parity.js.map +1 -0
  172. package/dist/agent/tools/notion_search.d.ts +56 -0
  173. package/dist/agent/tools/notion_search.d.ts.map +1 -0
  174. package/dist/agent/tools/notion_search.js +100 -0
  175. package/dist/agent/tools/notion_search.js.map +1 -0
  176. package/dist/agent/tools/read_notion_comments.d.ts +57 -0
  177. package/dist/agent/tools/read_notion_comments.d.ts.map +1 -0
  178. package/dist/agent/tools/read_notion_comments.js +236 -0
  179. package/dist/agent/tools/read_notion_comments.js.map +1 -0
  180. package/dist/agent/tools/read_notion_page.d.ts +53 -0
  181. package/dist/agent/tools/read_notion_page.d.ts.map +1 -0
  182. package/dist/agent/tools/read_notion_page.js +327 -0
  183. package/dist/agent/tools/read_notion_page.js.map +1 -0
  184. package/package.json +231 -0
  185. package/tsconfig.build.json +22 -0
  186. package/tsconfig.json +15 -0
@@ -0,0 +1,198 @@
1
+ // NOTION's event inventory (OPE-1612). See the core module's header
2
+ // (packages/core/agent/routines/provider-events.ts) for what the three routing states mean.
3
+ //
4
+ // NOTION PARTITIONS BY PREFIX, NOT BY EVENT. `notionEventClass`
5
+ // (channels/notion-event-normalizer.ts) reads the type's prefix: `page.` / `database.` /
6
+ // `data_source.` are STRUCTURAL and routable, `comment.` is CONVERSATION and the ambient
7
+ // supervisor's, anything else is unrecognised. `liftNotionEvent` returns null for everything that is
8
+ // not structural, so every `comment.` row below is honestly `routing: "none"` — nothing routes it,
9
+ // even though the bundle does register a normalizer.
10
+ //
11
+ // THE MAPPING IS THE ENVELOPE'S, NOT THE TYPE'S. `notionScopePath` reads `workspace_id`, the
12
+ // `parent` object and the `entity` object, and every structural delivery carries the same three. So
13
+ // each structural row below records the same mapping; what differs between them is what the event
14
+ // MEANS, which is what the note carries. The `sub` step is the exception and it is conditional:
15
+ // `page.content_updated` names an ARRAY of updated blocks and the path takes one only when the array
16
+ // holds exactly one, because the path holds at most one step per kind.
17
+ //
18
+ // 🔴 THE PARENT-TYPE SET IS THE NORMALIZER'S REVIEWED LIST, NOT THE VENDOR'S PUBLISHED ONE. Notion's
19
+ // event reference shows a `parent` object in each sample payload but never enumerates the types it
20
+ // can take, so `notionScopePath`'s list — `space` → `group`, and `database` / `data_source` / `page`
21
+ // / `block` → `container` — is a decision somebody made, and this file records the same set. A
22
+ // `space` parent is reachable for EVERY structural family, so every structural row below lists
23
+ // `group` beside `container`: the two are alternatives on one delivery (a parent is one thing), and
24
+ // listing both is what tells a reader that a teamspace-level narrowing is available at all.
25
+ //
26
+ // The `database.` family is the vendor's older spelling. Notion's 2025-09-03 API version deprecates
27
+ // several of those in favour of the `data_source.` equivalents; both are still delivered and both
28
+ // route identically here, so both are listed.
29
+
30
+ import type { ProviderEventDeclaration } from "@open-everywhere/core/agent/adapter-api.js";
31
+
32
+ export const NOTION_PROVIDER_EVENTS: ProviderEventDeclaration = {
33
+ source: "notion",
34
+ doc: "https://developers.notion.com/reference/webhooks-events-delivery",
35
+ events: [
36
+ {
37
+ event: "page.created",
38
+ routing: "mapped",
39
+ match: ["parent", "entity"],
40
+ scope: ["group", "container", "entity"],
41
+ note: "a page was added; the container step is its parent database, data source or page",
42
+ },
43
+ {
44
+ event: "page.properties_updated",
45
+ routing: "mapped",
46
+ match: ["parent", "entity"],
47
+ scope: ["group", "container", "entity"],
48
+ note: "a page's property values changed — a status field moving is this event, not a content edit",
49
+ },
50
+ {
51
+ event: "page.content_updated",
52
+ routing: "mapped",
53
+ match: ["parent", "entity"],
54
+ scope: ["group", "container", "entity", "sub"],
55
+ note: "a page's blocks changed; the sub step is the block, and only when the delivery names exactly one",
56
+ },
57
+ {
58
+ event: "page.moved",
59
+ routing: "mapped",
60
+ match: ["parent", "entity"],
61
+ scope: ["group", "container", "entity"],
62
+ note: "a page was moved to a different parent; the container step is the NEW parent",
63
+ },
64
+ {
65
+ event: "page.deleted",
66
+ routing: "mapped",
67
+ match: ["parent", "entity"],
68
+ scope: ["group", "container", "entity"],
69
+ note: "a page was sent to trash — the terminal event a scope path can carry and a fetch cannot",
70
+ },
71
+ {
72
+ event: "page.undeleted",
73
+ routing: "mapped",
74
+ match: ["parent", "entity"],
75
+ scope: ["group", "container", "entity"],
76
+ note: "a page was restored from trash",
77
+ },
78
+ {
79
+ event: "page.locked",
80
+ routing: "mapped",
81
+ match: ["parent", "entity"],
82
+ scope: ["group", "container", "entity"],
83
+ note: "a page was locked against edits",
84
+ },
85
+ {
86
+ event: "page.unlocked",
87
+ routing: "mapped",
88
+ match: ["parent", "entity"],
89
+ scope: ["group", "container", "entity"],
90
+ note: "a page was unlocked",
91
+ },
92
+ {
93
+ event: "database.created",
94
+ routing: "mapped",
95
+ match: ["parent", "entity"],
96
+ scope: ["group", "container", "entity"],
97
+ note: "a database was added; the group step is the teamspace, when the parent is one",
98
+ },
99
+ {
100
+ event: "database.content_updated",
101
+ routing: "mapped",
102
+ match: ["parent", "entity"],
103
+ scope: ["group", "container", "entity"],
104
+ note: "rows were added to or removed from a database",
105
+ },
106
+ {
107
+ event: "database.schema_updated",
108
+ routing: "mapped",
109
+ match: ["parent", "entity"],
110
+ scope: ["group", "container", "entity"],
111
+ note: "a database's property schema changed — the event that breaks a routine reading a column by name",
112
+ },
113
+ {
114
+ event: "database.moved",
115
+ routing: "mapped",
116
+ match: ["parent", "entity"],
117
+ scope: ["group", "container", "entity"],
118
+ note: "a database was moved to a different parent",
119
+ },
120
+ {
121
+ event: "database.deleted",
122
+ routing: "mapped",
123
+ match: ["parent", "entity"],
124
+ scope: ["group", "container", "entity"],
125
+ note: "a database was sent to trash",
126
+ },
127
+ {
128
+ event: "database.undeleted",
129
+ routing: "mapped",
130
+ match: ["parent", "entity"],
131
+ scope: ["group", "container", "entity"],
132
+ note: "a database was restored from trash",
133
+ },
134
+ {
135
+ event: "data_source.created",
136
+ routing: "mapped",
137
+ match: ["parent", "entity"],
138
+ scope: ["group", "container", "entity"],
139
+ note: "the 2025-09-03 spelling of a database creation, delivered beside the older `database.` family",
140
+ },
141
+ {
142
+ event: "data_source.content_updated",
143
+ routing: "mapped",
144
+ match: ["parent", "entity"],
145
+ scope: ["group", "container", "entity"],
146
+ note: "rows were added to or removed from a data source",
147
+ },
148
+ {
149
+ event: "data_source.schema_updated",
150
+ routing: "mapped",
151
+ match: ["parent", "entity"],
152
+ scope: ["group", "container", "entity"],
153
+ note: "a data source's property schema changed",
154
+ },
155
+ {
156
+ event: "data_source.moved",
157
+ routing: "mapped",
158
+ match: ["parent", "entity"],
159
+ scope: ["group", "container", "entity"],
160
+ note: "a data source was moved to a different parent",
161
+ },
162
+ {
163
+ event: "data_source.deleted",
164
+ routing: "mapped",
165
+ match: ["parent", "entity"],
166
+ scope: ["group", "container", "entity"],
167
+ note: "a data source was sent to trash",
168
+ },
169
+ {
170
+ event: "data_source.undeleted",
171
+ routing: "mapped",
172
+ match: ["parent", "entity"],
173
+ scope: ["group", "container", "entity"],
174
+ note: "a data source was restored from trash",
175
+ },
176
+ {
177
+ event: "comment.created",
178
+ routing: "none",
179
+ match: [],
180
+ scope: [],
181
+ note: "a comment was added; declined by the normalizer on purpose — a comment is CONVERSATION and the ambient supervisor answers it, so routing it would wake a seat twice for one event",
182
+ },
183
+ {
184
+ event: "comment.updated",
185
+ routing: "none",
186
+ match: [],
187
+ scope: [],
188
+ note: "a comment was edited; declined with the whole `comment.` family",
189
+ },
190
+ {
191
+ event: "comment.deleted",
192
+ routing: "none",
193
+ match: [],
194
+ scope: [],
195
+ note: "a comment was removed; declined with the whole `comment.` family",
196
+ },
197
+ ],
198
+ };
@@ -0,0 +1,144 @@
1
+ // registerNotionProvider — the ONE boot entry of @open-everywhere/provider-notion. The app's
2
+ // adapter-wiring module (agent/lib/register-adapters.ts) imports this STATICALLY and calls it once
3
+ // at boot; every seam the notion bundle implements registers here and nowhere else. Static imports
4
+ // only: eve bundles the agent definition and its whole import graph into one rolldown chunk — a
5
+ // literal dynamic import() code-splits and trips eve's "expected one authored module" invariant.
6
+ //
7
+ // What notion registers (and what it deliberately does NOT):
8
+ // • identity mapper — bindNotion (chat-surface plane; channels/notion-identity.ts)
9
+ // • capability manifest — midTurnPost wired via createCommentReply; no reaction primitive
10
+ // • post arm — notionPostArm (mid-turn reply + unified reply + edit + redrive helpers)
11
+ // • wake-origin codec — the stored-data notion arm (channels/notion-origin-codec.ts)
12
+ // • webhook verifier — Notion's `x-notion-signature` scheme (channels/notion-webhook-verifier.ts)
13
+ // • event normalizer + — the routing plane's two halves, STRUCTURAL events only; comments stay
14
+ // event scope grant with the ambient supervisor (channels/notion-event-normalizer.ts)
15
+ // • subtree platform — Notion honours `Surface.subtree`, and registerNotionTrustRoots is
16
+ // what core hands the declared keys to (channels/notion-ancestry-trust.ts)
17
+ // • memory provider — the read-only Notion page-subtree backing (memory/memory.ts)
18
+ // • read adapter — notionReadAdapter (channels/notion-read.ts, OPE-988): THREAD-FIRST —
19
+ // read_thread = one discussion (the default view), read_channel = the
20
+ // page's discussions, each headed by its anchor. Page CONTENT is not on
21
+ // the chat plane; the comment layer is Notion's chat surface.
22
+ // • ancillary message ops — notionMessageOps (channels/notion-message-ops.ts, OPE-1043/OPE-1047):
23
+ // postText/editMessage — the transport the stop acknowledgement
24
+ // (OPE-989's control) and the approval-decision notice now have on
25
+ // Notion. postFiles uploads through Notion's two-step file-upload API
26
+ // (notion-api.ts's uploadNotionFile) and attaches the results to a page
27
+ // comment. NO setReaction (no reaction primitive, below).
28
+ // • NO reaction surface — Notion has no integration reaction primitive
29
+ // • NO grant plane arm — personal lending BORROWS the owner's MCP tools (OPE-1136). Core
30
+ // drives that path itself through the connector's own MCP connection
31
+ // (connections/grant-mcp-borrow.ts); a provider bundle registers
32
+ // nothing for it. The REST arm this file once carried — a verb
33
+ // registry plus a `GrantTransport` spending a second REST-family
34
+ // token — is deleted.
35
+
36
+ import {
37
+ registerIdentityMapper,
38
+ registerCapabilityManifest,
39
+ registerSurfaceParity,
40
+ registerProviderEvents,
41
+ registerPostArm,
42
+ registerWakeOriginCodec,
43
+ registerReadAdapter,
44
+ registerSeatCatchUp,
45
+ registerProviderDocs,
46
+ registerWebhookVerifier,
47
+ registerEventNormalizer,
48
+ registerEventScopeGrant,
49
+ registerSubtreePlatform,
50
+ registerSurfaceMessageOps,
51
+ } from "@open-everywhere/core/agent/adapter-api.js";
52
+ import { registerNotionTrustRoots } from "./channels/notion-ancestry-trust.js";
53
+ import { PROVIDER_DOCS } from "./docs-generated.js";
54
+ import { NOTION_SURFACE_PARITY } from "./surface-parity.js";
55
+ import { NOTION_PROVIDER_EVENTS } from "./provider-events.js";
56
+ import { bindNotion } from "./channels/notion-identity.js";
57
+ import { notionPostArm } from "./channels/notion-post-arm.js";
58
+ import { notionMessageOps } from "./channels/notion-message-ops.js";
59
+ import { notionWakeOriginCodec } from "./channels/notion-origin-codec.js";
60
+ import { notionReadAdapter } from "./channels/notion-read.js";
61
+ import { notionSeatCatchUpFetch, NOTION_TIMESTAMP_PRECISION_MS } from "./channels/notion-catch-up.js";
62
+ import { registerNotionWorkSearch } from "./channels/notion-work-search.js";
63
+ import { notionWebhookVerifier } from "./channels/notion-webhook-verifier.js";
64
+ import { notionEventNormalizer, notionEventScopeGrant } from "./channels/notion-event-normalizer.js";
65
+ import { registerNotionMemory } from "./memory/memory.js";
66
+
67
+ /** Perform ALL of the notion bundle's registrations. Idempotent (each register call replaces);
68
+ * cheap and side-effect-free beyond the registry writes (the memory provider's token resolves
69
+ * lazily through the credential broker only when a notion silo is actually read). */
70
+ export function registerNotionProvider(): void {
71
+ registerIdentityMapper("notion", bindNotion);
72
+ // Comment surface: mid-turn post wired via createCommentReply (the post arm's dispatchReply
73
+ // posts a new comment into the triggering discussion). No integration reaction primitive.
74
+ // closingProseDropped (OPE-811, widened by OPE-2006): a per-doc container-supervisor turn runs the
75
+ // message-tool-only gate live — it did already on a windowed domain, and now on every domain — so
76
+ // its turn-final text is dropped and `reply` is the only route out.
77
+ // 🔴 THE SURFACE-WIDE FACT NEEDS SURFACE-WIDE EVIDENCE, and here it is: `channels/notion.ts`'s
78
+ // `message.completed` is this bundle's ONLY completion handler, it routes EVERY Notion session
79
+ // through the shared `completeSupervisorTurn` arm whatever the session's shape, and it never
80
+ // delegates to the agent framework's default poster. So no Notion seat of any shape posts its
81
+ // closing prose — which is the bar `identity/capabilities.ts` sets for declaring this fact, and
82
+ // the bar Linear fails.
83
+ registerCapabilityManifest("notion", { midTurnPost: true, reactions: false, closingProseDropped: true });
84
+ // The OPE-1499 parity ledger: every capability the chat-surface contract names, answered
85
+ // `supported` or `{ unsupported: "<reason>" }`. Gates nothing at runtime — `lint:surface-parity`
86
+ // reds on an unanswered row, and the contract's §7.1/§7.1.1/§7.2 tables are generated from it.
87
+ registerSurfaceParity("notion", NOTION_SURFACE_PARITY);
88
+ // The OPE-1612 event inventory: every vendor event that could wake a routine, and how far the
89
+ // normalizer plane carries each one. Gates nothing at runtime — `lint:provider-events` reds a
90
+ // bundle that declares none or that promises a match key the normalizer does not declare, and
91
+ // the manual's inventory table is generated from it.
92
+ registerProviderEvents("notion", NOTION_PROVIDER_EVENTS);
93
+ registerPostArm(notionPostArm);
94
+ // Ancillary message ops (OPE-1043): postText/editMessage, so the stop acknowledgement
95
+ // (hooks/turn-anchor.ts's postSurfaceText) and the approval-decision notice have a transport on
96
+ // Notion. postFiles (OPE-1047) uploads via Notion's file-upload API. No setReaction (no
97
+ // reaction primitive — see below).
98
+ registerSurfaceMessageOps(notionMessageOps);
99
+ registerWakeOriginCodec("notion", notionWakeOriginCodec);
100
+ // Generic chat READ plane (OPE-988): read_channel / read_thread over the page's comment layer.
101
+ // Without this the two verbs still MOUNTED on a unified window and refused at execute time
102
+ // ("reading a notion channel is not supported yet") — the OPE-927 mount-honesty class.
103
+ registerReadAdapter(notionReadAdapter);
104
+ // THE SEAT CATCH-UP FETCH (OPE-1726): the comments this page discussion collected since the
105
+ // seat's last wake. Core's shared path applies the bounds, the admission policy and the cap.
106
+ // The surface DECLARES the unit its own message stamps carry, and core widens both catch-up
107
+ // bounds by it (OPE-1847). A wrong or missing unit bounds the seat out of its own window.
108
+ registerSeatCatchUp("notion", notionSeatCatchUpFetch, { timestampPrecisionMs: NOTION_TIMESTAMP_PRECISION_MS });
109
+ // Work-plane federated SEARCH (OPE-961): the ONE `search` tool's Notion half. Grant-gated like
110
+ // notion_search.ts, and now registered here too so the federator can find it.
111
+ registerNotionWorkSearch();
112
+ // Webhook-verifier plane (OPE-164): Notion's `x-notion-signature` scheme, so a GENERIC hook can
113
+ // declare `source: "notion"` and be verified the way Notion signs. Changes no existing route.
114
+ registerWebhookVerifier(notionWebhookVerifier);
115
+ // ⚠️ NO PROVISIONING DECLARATION, AND THAT IS NOT AN OMISSION (OPE-207). Notion's signing key is
116
+ // its own `verification_token`, which arrives once in an unauthenticated setup POST — so it can
117
+ // never be a key WE mint, and `notionWebhookVerifier`'s `keyOrigin: "vendor-issued"` is what keeps
118
+ // `source: "notion"` out of the chat-creatable vocabulary. A declaration + setup exchange for that
119
+ // arm shipped and was deleted: nothing could put a hook into the status the arm required, so it was
120
+ // reachable by no route. A Notion hook is a CONFIG hook (its secret arrives through
121
+ // `webhooks/handshake-capture.ts` on the provider's own front door) until that changes.
122
+ // Event plane (OPE-189) — the FIRST bundle-resident normalizer + scope grant in the tree. Both
123
+ // registered on ADJACENT lines on purpose: `checkEventReadGrant` fail-closes to
124
+ // `no-grant-adapter` when a grant is missing, so a normalizer registered ALONE would produce a
125
+ // source where every match is skipped with an audited refusal and nothing says why.
126
+ // The normalizer routes `page.*`/`database.*`/`data_source.*` (work) and returns null for
127
+ // `comment.*` (conversation — the ambient supervisor's, above); see notion-event-normalizer.ts.
128
+ registerEventNormalizer(notionEventNormalizer);
129
+ registerEventScopeGrant(notionEventScopeGrant);
130
+ // Subtree-trust plane (OPE-539): Notion is the one platform whose surface key carries no
131
+ // hierarchy — a page id names a page that can hold pages — so it is the one platform that honours
132
+ // `defineSurface({ …, subtree: true })`. Core carries the claim and hands the keys here; this
133
+ // module walks the parent chain at ingestion. Declaring the capability is also what makes the same
134
+ // word on slack/github/teams a BOOT ERROR instead of a line that grants nothing.
135
+ registerSubtreePlatform("notion", registerNotionTrustRoots);
136
+ // Memory plane: a read-only silo with provider:"notion", container:"<root-page-id>" resolves to
137
+ // that Notion page + all its child pages, pulled recursively (docs/capabilities/memory.md). The
138
+ // integration token resolves LAZILY through the credential broker (default ref NOTION_TOKEN).
139
+ registerNotionMemory();
140
+ // Shipped-manual plane (OPE-138): this bundle's own setup runbook (docs/deploy/surfaces/notion.md,
141
+ // compiled into ./docs-generated.ts because docs/ is not on disk at runtime). De-select the bundle
142
+ // and the module leaves the import graph; registerProviderDocs re-checks the selection gate anyway.
143
+ registerProviderDocs("notion", PROVIDER_DOCS);
144
+ }
@@ -0,0 +1,20 @@
1
+ // Recovery clock for authenticated Notion deliveries. The public route nudges the same drain
2
+ // immediately, but this minute tick is the durable guarantee after a process dies between commit
3
+ // and that nudge. It sends row keys only; the private route atomically claims and loads payload.
4
+
5
+ import { defineSchedule } from "eve/schedules";
6
+ import { deploymentOrg, getOperationalStore } from "@open-everywhere/core/agent/adapter-api.js";
7
+
8
+ import { recoverNotionIngress } from "../channels/notion-ingress-drain.js";
9
+
10
+ export default defineSchedule({
11
+ cron: "* * * * *",
12
+ async run() {
13
+ const result = await recoverNotionIngress(deploymentOrg(), getOperationalStore());
14
+ if (result.due > 0) {
15
+ console.log(
16
+ `[open-everywhere:notion] ingress recovery: ${result.due} due -> ${result.posted} posted, ${result.failed} failed`,
17
+ );
18
+ }
19
+ },
20
+ });
@@ -0,0 +1,105 @@
1
+ // NOTION's surface-parity declaration (OPE-1499). See the core module's header
2
+ // (packages/core/agent/identity/surface-parity.ts) for what the three cell shapes mean.
3
+ //
4
+ // Notion is THREAD-FIRST and the inverse of Slack: a comment carries `discussion_id` from birth, a
5
+ // new top-level comment MINTS a discussion, and there is no bare-channel state at all (§4.6.1). It
6
+ // is also the one surface with no @mention of an integration, so its addressed leg keys on a
7
+ // name-drop or structured mention instead — which is why those two rows read the way they do.
8
+
9
+ import type { SurfaceParityDeclaration } from "@open-everywhere/core/agent/adapter-api.js";
10
+
11
+ export const NOTION_SURFACE_PARITY: SurfaceParityDeclaration = {
12
+ // --- registration seams (§7.1) ---------------------------------------------------------------
13
+ "identity-mapper": "supported",
14
+ "capability-manifest": "supported",
15
+ "post-arm": "supported",
16
+ "wake-origin-codec": "supported",
17
+ "unified-action-plane": {
18
+ unsupported:
19
+ "a Notion discussion comment is rich text with no component plane — the API offers no tappable button an integration may render into one, so there is nothing for a tap route to receive. The bridged approval posts as prose, which the question bridge already renders complete",
20
+ },
21
+ "origin-placement-carry": {
22
+ unsupported:
23
+ "the placement is INSIDE the conversation identity: a reply targets `discussionId`, which is exactly what `threadRef` keys on, so two origins that pass the conversation test name the same discussion and cannot disagree about placement",
24
+ },
25
+ "read-adapter": "supported",
26
+ // 🔴 PERMANENT, and confirmed in BOTH directions (OPE-1502, developers.notion.com read
27
+ // 2026-09-07). The webhook catalogue carries page, database, data-source and comment events and
28
+ // no reaction event of any kind; the comment object carries `rich_text`, `attachments` and
29
+ // `display_name` and no reactions field. So there is nothing to receive and nothing to poll —
30
+ // unlike GitHub, where the absent webhook still leaves a REST route to read.
31
+ "reaction-feedback-ingest": { unsupported: "vendor limit — Notion pushes no reaction event and its comment object exposes no reaction resource, so there is nothing to receive and nothing to poll" },
32
+ "ancillary-ops": "supported",
33
+ "ambient-engagement-seam": "supported",
34
+ // `notionWakeAddressing` (channels/notion-engagement.ts) carries the seam's own `deliver` verdict
35
+ // to the `notion.dispatched` row, so an engaged follow-up reads `thread-follow` and ambient page
36
+ // chatter still reads `channel-watch`. The row also carries `via`, which names the DOMAIN ORIGIN
37
+ // the addressed door gated on — that field is Notion's alone (OPE-1510).
38
+ "engaged-row-promotion": "supported",
39
+ // Notion is thread-first: every wake arrives scoped to one discussion, so every wake this surface
40
+ // diverts is thread-scoped and consults ownership.
41
+ "owned-thread-ingress": "supported",
42
+
43
+ // --- wake legs and conversation shapes -------------------------------------------------------
44
+ // Notion lets no user @mention an internal integration, so the old mention gate was structurally
45
+ // dead (channels/notion-webhook.ts). A `direct` Notion wake is an ADDRESSED, single-author
46
+ // comment recognised by a name-drop or structured mention — the leg exists, by another route.
47
+ "addressed-leg": { supported: "the `direct` wake — a name-drop or structured mention, not a platform @mention" },
48
+ // The ambient supervisor, a different mechanism from the §2.5 registry: the domain opt-in decides
49
+ // whether it runs, and the registry has never gated that. Since OPE-1501 the registry both
50
+ // SUPPRESSES it (a stood-down page) and OPENS it (an engaged page wakes without the opt-in).
51
+ "ambient-leg": { supported: "the ambient supervisor, not the §2.5 engagement registry" },
52
+ // `notion-catch-up.ts` re-reads the seat's own discussion (a single block's inline comments) over
53
+ // the same brokered credential `read_channel` uses, keeping what is newer than the seat's own
54
+ // `lastWakeAt`. Notion's comments endpoint takes no `since`, so the bound is applied on our side.
55
+ "seat-catch-up": "supported",
56
+ // The slot is EMPTY on this surface: `notion.ts` builds no `contextLines`, so there is no pour to
57
+ // police. The seat's room context is the catch-up read above, which core already admits.
58
+ "seat-context-admission": { unsupported: "the hydration fills no context-line slot — the seat's room context is the catch-up read, which core already admits" },
59
+ // A Notion comment carries no component plane — no button, no tap route. See the roster row for
60
+ // what that costs and where it does NOT cost anything.
61
+ "approval-card": { unsupported: "a page comment carries no component plane, so there is no button to render and no route for a tap. A governed act that takes the venue walk is redirected to a card-capable room in the SAME domain, or refused when the domain holds none. The paths OUTSIDE that walk do not: the spend checkpoint (spend/checkpoint.ts `requireSessionContinuation`) fails OPEN here and records the crossing in an audit row instead" },
62
+ // THE TRACE ARRIVES EVEN THOUGH THE BUTTON CANNOT (OPE-1854). A judge-lifted act posts its
63
+ // notice here as a plain page comment through the shared road (`postJudgeLiftNotice`), on this
64
+ // surface's own `SurfaceMessageOps` transport. It carries the overrule as an INSTRUCTION in
65
+ // words, with the action id a reviewer finds the act by, because no tap can land here.
66
+ // 🔴 BARE, NOT `{ supported: "…" }`. A noted cell is exempt from the overclaim arm, and the
67
+ // arm is the point: drop this bundle's text transport and the notice silently stops arriving.
68
+ "judge-lift-notice": "supported",
69
+ "name-drop-mention": "supported",
70
+ // 🔴 STEP 4 OF THE WEBHOOK ADMITS ONE EVENT TYPE. `handleNotionDelivery` answers `ignored` for
71
+ // anything that is not `comment.created`, so `comment.updated` returns above the classifier, the
72
+ // ambient gate and the engagement seam alike.
73
+ "message-edit-wake": { unsupported: "the webhook admits `comment.created` only — `comment.updated` answers `ignored` at step 4" },
74
+ // ONE discussion. A Notion wake always arrives thread-scoped (§4.6.1).
75
+ threads: { supported: "thread-first — every wake arrives scoped to one discussion" },
76
+ // A STANDING room, not §4.5's read_channel container — Notion has the latter (the page's
77
+ // discussions) and `read_channel` is supported below. A discussion is minted by its first comment
78
+ // and is anchored to one block, so nothing on this surface predates the conversation in it.
79
+ rooms: { unsupported: "no standing container — a discussion is minted by its first comment and anchored to one block" },
80
+ dm: { unsupported: "no DM plane — Notion carries no person-to-person message channel" },
81
+ mpim: { unsupported: "no DM plane — Notion carries no person-to-person message channel" },
82
+
83
+ // --- core verbs (§7.2) -----------------------------------------------------------------------
84
+ reply: "supported",
85
+ edit_reply: "supported",
86
+ react: { unsupported: "vendor limit — the Notion comment API exposes no reaction write" },
87
+ no_reply: "supported",
88
+ read_channel: "supported",
89
+ read_thread: "supported",
90
+ search: "supported",
91
+ // Unbuilt, not impossible — see OPE-1507. The transport is registered; the missing piece is the
92
+ // `channel` row in `access/resource-grant.ts`'s closed `RESOURCE_TYPES` table.
93
+ post_message: { unsupported: "not built yet — no `channel` row in `access/resource-grant.ts` — OPE-1507" },
94
+ dm_person: { unsupported: "no DM plane — Notion carries no person-to-person message channel (§4.7.2)" },
95
+ set_dm_delivery: { unsupported: "no DM plane — the verb mounts only inside a 1:1 DM (§4.7.2)" },
96
+ // The direction matters: a human's stop needs an INBOUND reaction event, and Notion's webhook
97
+ // catalogue carries none (notion-stop.ts's banner is the vendor evidence). Whether OE can WRITE a
98
+ // reaction is a different limit, and it is the one the two rows below name.
99
+ stop: { supported: "a comment whose entire body is `stop` — Notion pushes no reaction webhook" },
100
+ ack_stop_glyph: { unsupported: "vendor limit — the Notion comment API exposes no reaction write, so there is no glyph to place" },
101
+ ack_cancel_note: "supported",
102
+ stand_down: { supported: "marks the page row; refuses when no row exists" },
103
+ // OPE-1047. Notion accepts at most three files per comment — its own vendor cap.
104
+ post_file: { supported: "at most 3 files per post — a Notion comment's own vendor cap" },
105
+ };
@@ -0,0 +1,110 @@
1
+ // THE WORK-PLANE SEARCH (OPE-913) — Notion's own cross-container read, under a domain work grant.
2
+ //
3
+ // WHY IT IS WORK AND NOT CHAT (OPE-928's container line): a read INSIDE the wake container is chat —
4
+ // the envelope already renders it. A read ACROSS containers is work: it reaches pages nobody in this
5
+ // conversation named, which is an exfiltration surface, so it lives behind the domain's work grant
6
+ // and runs on the work credential.
7
+ //
8
+ // MOUNT: the domain's work grant, and nothing else. No grant ⇒ the tool is ABSENT, not refusing.
9
+ // That is the OPE-927 class closed structurally: a worker that never mounts this cannot tell a
10
+ // member it "searched Notion and found nothing".
11
+ //
12
+ // 🔴 NO RESULT FILTER, EVER. Tommy ruled on 2026-08-21 that search's reach IS the credential's
13
+ // reach: "OE never filters search results as a boundary (a post-hoc filter is not enforcement)."
14
+ // Notion enforces the boundary for real — an internal integration sees only the page trees a human
15
+ // shared with it — so an organisation narrows search by sharing a narrow integration, and OE ships
16
+ // the legible seam rather than a filter that would look like one. If you are about to add a
17
+ // domain-scoped filter here, read that ruling first.
18
+ //
19
+ // WHAT COMES BACK: ids and titles. No page body ever crosses this tool, so the model has to take the
20
+ // second, FENCED step (read_notion_page, which admits only pages the caller's own domain owns) to
21
+ // read anything. Search widens what the agent can FIND; it does not widen what it can READ.
22
+ //
23
+ // Output is wrapped untrusted: a page title is written by arbitrary workspace members, so the model
24
+ // must treat it as DATA, not instructions.
25
+
26
+ import { defineDynamic, defineTool } from "eve/tools";
27
+ import { z } from "zod";
28
+ import { resolveNotionCredential, searchNotion } from "../channels/notion-api.js";
29
+ import { notionWorkGrant, notionWorkMount } from "../channels/notion-work.js";
30
+ import { assertNotionActingIdentity } from "../channels/notion-acting-identity.js";
31
+ import {
32
+ ingestUntrustedModelOutput,
33
+ getConfig,
34
+ preRegisterDynamicEntry,
35
+ type BindableContext,
36
+ } from "@open-everywhere/core/agent/adapter-api.js";
37
+
38
+ const MAX_RESULTS = 25;
39
+
40
+ // FACTORED (see read_notion_page.ts's REPLAY DOCTRINE note): a top-level defineTool const, never an
41
+ // inline literal in the handler. Exported so the work-grant test can drive execute() and prove the
42
+ // grant is actually WIRED here — a mount gate the handler forgets to honour is worth nothing.
43
+ export const notionSearchEntry = defineTool({
44
+ description:
45
+ "Search this domain's Notion workspace by title and text, and get back page ids + titles. " +
46
+ "Use it to FIND the page you need — a spec, a runbook, a notes page — then read it with " +
47
+ "read_notion_page. It returns no page content, so a result is a pointer, not an answer.",
48
+ inputSchema: z.object({
49
+ query: z.string().min(1).describe("Words to search for in page titles and text."),
50
+ limit: z.number().int().min(1).max(MAX_RESULTS).optional().describe(`How many results to return (default 10, max ${MAX_RESULTS}).`),
51
+ kind: z.enum(["page", "database"]).optional().describe("Restrict results to pages or to databases. Omit for both."),
52
+ }),
53
+ outputSchema: z.object({
54
+ query: z.string(),
55
+ hits: z.array(z.object({ id: z.string(), object: z.string(), title: z.string(), url: z.string().optional() })),
56
+ }),
57
+ // NO `approval:` — a dynamic-tool approval fn only registers at session.started resolve time, so a
58
+ // resumed session's replay lookup fails and eve kills the turn (the pin read_notion_page carries).
59
+ // Read-only, so absent approval === never() is correct.
60
+ async execute(input, ctx) {
61
+ const domains = getConfig().loadDomains();
62
+ // THE GRANT IS RE-CHECKED IN EXECUTE, not trusted from the mount. A session resolves its tools
63
+ // once at session.started and can then run for hours; the config it mounted under is a
64
+ // deployment away from changing. Re-reading it here costs one pure lookup and means a revoked
65
+ // grant stops working on the next call rather than on the next rotation.
66
+ const grant = notionWorkGrant(ctx as BindableContext, domains);
67
+ if (!grant) {
68
+ throw new Error(
69
+ "This domain holds no Notion work grant, so Notion search is unavailable here. " +
70
+ "An operator grants it by declaring the domain's Notion work credential in agent/memory/config/domains.ts.",
71
+ );
72
+ }
73
+ // 🔴 OPE-1525 — THE OWN-ACCOUNT GATE, on the line below's behalf. `grant.credential` is a TEAM
74
+ // key by construction (channels/notion-work.ts NOTION_WORK_CREDENTIAL), so a member who asked to
75
+ // act with THEIR OWN Notion account has no answer here. The gate refuses that turn and writes the
76
+ // `connector.acting_identity` row either way — see channels/notion-acting-identity.ts.
77
+ await assertNotionActingIdentity(ctx as BindableContext, "notion_search");
78
+ const cred = await resolveNotionCredential(grant.credential);
79
+ if (!cred) throw new Error(`no value stored for this domain's Notion work credential "${grant.credential}" — cannot search.`);
80
+ const hits = await searchNotion(input.query, {
81
+ ...cred,
82
+ pageSize: input.limit ?? 10,
83
+ ...(input.kind ? { filter: input.kind } : {}),
84
+ });
85
+ return { query: input.query, hits: hits.slice(0, input.limit ?? 10).map((h) => ({ ...h })) };
86
+ },
87
+ toModelOutput(output) {
88
+ const body =
89
+ output.hits.length === 0
90
+ ? `Notion search for "${output.query}" matched nothing the work integration can see.`
91
+ : `Notion search for "${output.query}" — ${output.hits.length} result(s):\n` +
92
+ output.hits.map((h) => `- ${h.object} ${h.id}: ${h.title || "(untitled)"}`).join("\n");
93
+ return ingestUntrustedModelOutput(body, {
94
+ source: "notion search",
95
+ ingestion: { kind: "notion", label: "notion_search" },
96
+ });
97
+ },
98
+ });
99
+ preRegisterDynamicEntry("notion_search", "_single", notionSearchEntry);
100
+
101
+ export default defineDynamic({
102
+ events: {
103
+ // session.started ONLY: a session's domain cannot change mid-session, so resolving the grant
104
+ // once is correct and cheaper (the same reasoning read_notion_page's resolver carries).
105
+ "session.started": (_event, ctx) => {
106
+ if (!notionWorkMount(ctx as BindableContext, () => getConfig().loadDomains())) return null; // no work grant ⇒ absent
107
+ return notionSearchEntry;
108
+ },
109
+ },
110
+ });