@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,360 @@
1
+ // WHICH NOTION INTEGRATION SERVES THIS PAGE (OPE-467) โ€” the one place the Notion bundle decides
2
+ // which credential a call runs under, and which integrations may sign an inbound delivery.
3
+ //
4
+ // THE PROBLEM. Notion the vendor supports MANY internal integrations, and each one only sees the
5
+ // page trees a human explicitly shared with it. That is a real, vendor-enforced boundary โ€” the
6
+ // strongest isolation any of OE's six surfaces can buy. Until this module OE wired ONE integration
7
+ // for every domain, so the boundary was app-layer only: `notion-scope.ts` fenced the domain, and
8
+ // the credential underneath could see every page in the workspace. `notion-scope.ts` still says so
9
+ // in its own header, and it stays true for a domain on the org default.
10
+ //
11
+ // THE RULE FOR WORK THE AGENT INITIATES โ€” a read tool, a proactive post: **the domain that owns the
12
+ // page decides the credential.**
13
+ // โ€ข the page's domain declares a `credentials` override for notion `channel-key` โ‡’ that credential;
14
+ // โ€ข it declares none (or the page is in no domain) โ‡’ the org default `channel-key`.
15
+ //
16
+ // THE RULE FOR A DELIVERY, and for everything downstream of one: **the integration that VERIFIED it
17
+ // serves it.** Each integration signs its deliveries with its OWN vendor-issued
18
+ // `verification_token`, so a second integration's deliveries cannot verify against the first's
19
+ // secret. {@link notionIntegrations} enumerates the configured integrations and the route tries each
20
+ // one's secret in turn โ€” the same "ordered keys, current first" shape `webhooks/secrets.ts` uses for
21
+ // a rotation window โ€” and THE SECRET THAT VERIFIES IDENTIFIES THE INTEGRATION THAT SENT THE
22
+ // DELIVERY. That is the only authenticated fact a delivery carries.
23
+ //
24
+ // ๐Ÿ”ด THAT RULE REACHES THE READS TOO (OPE-499, ruled by Tommy 2026-08-13). A delivery-woken session's
25
+ // READ TOOLS run under the SAME carried credential as its fetch and its reply โ€” never a fresh lookup
26
+ // off the page's domain binding. {@link notionCallerCredential} reads the `credential_id` auth
27
+ // attribute the delivery stowed (notion-identity.ts buildNotionAuth) and prefers it when present. The
28
+ // domain binding stays the rule for an AGENT-INITIATED session โ€” a routine, a proactive post โ€” where
29
+ // no delivery ever ran and there is no carried credential to prefer.
30
+ //
31
+ // WHY THIS CLOSES A REAL GAP. An operator who shares one page with BOTH the org-default integration
32
+ // and a domain's own integration lets either subscription's `verification_token` holder produce a
33
+ // verified delivery for that page. Before this rule, the session that delivery woke still read under
34
+ // the PAGE's domain binding โ€” so a holder of the WEAKER (org) credential could ride a double-shared
35
+ // page into every page the domain's OWN, narrower integration can see. Carrying the delivery's own
36
+ // credential into the reads means a session only ever reads what the credential that actually
37
+ // verified it was shared into โ€” the vendor's own share-list boundary, working as designed.
38
+ //
39
+ // ๐Ÿ”ด WHY A BINDING DOES NOT OUTRANK THE HMAC. A delivery's page id is read off the payload, and
40
+ // nothing authenticates the payload. If the page's binding decided, a forger holding integration
41
+ // A's `verification_token` could name a domain-B page and have the delivery run under B's
42
+ // credential. So what a binding actually buys on ingress is narrower than "this page is served by
43
+ // this integration": **a binding routes the deliveries the bound subscription itself sent.** Both
44
+ // rules meet at one credential id per delivery, and the REPLY carries that id forward (the auth
45
+ // attributes stow it) โ€” so the credential that fetched a comment is the credential that answers it,
46
+ // on every path.
47
+ //
48
+ // A page shared with two integrations produces a delivery on EACH subscription. That is legitimate,
49
+ // and each delivery correctly runs under its own verifying integration โ€” which is why a disagreement
50
+ // between the HMAC and the binding PREFERS the HMAC rather than refusing.
51
+ //
52
+ // ๐Ÿ”ด WHAT THIS DOES NOT BUY. Binding a domain to its own integration only fences the pages that
53
+ // integration was SHARED into. If the operator leaves a payments page shared with the org-default
54
+ // integration as well, that key can still read it โ€” Notion enforces the share list, and OE cannot
55
+ // widen or narrow it. The runbook says this plainly: bind the domain AND unshare the page from
56
+ // every other integration.
57
+ import { domainCredentialId, OperatorSecretNotSetError, resolveDomainForChannel, connectorIntegrations, resolvePartition, } from "@open-everywhere/core/agent/adapter-api.js";
58
+ import { notionSurfaceKey } from "./notion-identity.js";
59
+ import { notionPageDomain } from "./notion-scope.js";
60
+ /** The provider name in `agent/access/providers.ts`. */
61
+ export const NOTION_PROVIDER = "notion";
62
+ /** The ORG-DEFAULT channel-plumbing credential id โ€” the role a domain override replaces. */
63
+ export const NOTION_CHANNEL_CREDENTIAL = "channel-key";
64
+ /** The operator-secret name holding the DEFAULT integration's inbound signing key (Notion's
65
+ * subscription `verification_token`). A domain that runs its own subscription names its own with
66
+ * `inboundSecret`. Resolved through the operator-secret resolver, so a value stored with
67
+ * `oe secret set NOTION_WEBHOOK_SECRET` wins over an env var of the same name. */
68
+ const NOTION_WEBHOOK_SECRET_NAME = "NOTION_WEBHOOK_SECRET";
69
+ /**
70
+ * The notion `channel-key` credential id a page's domain binds, or **undefined when it binds none**.
71
+ *
72
+ * Undefined is a load-bearing answer, not a failure: it means "this page's domain asked for no
73
+ * override", which every caller reads as "use the org default". Keeping it distinct from a resolved
74
+ * id is what lets the webhook route fall through to its own arms (the verifying integration, then
75
+ * the org default) instead of treating "no binding" as a refusal.
76
+ *
77
+ * Pure โ€” `domains` is injected, exactly like `notionPageDomain`'s, so the whole decision is
78
+ * unit-testable with no config and no store.
79
+ */
80
+ export function notionChannelCredentialFor(workspace, pageId, domains) {
81
+ if (!pageId)
82
+ return undefined;
83
+ const domain = notionPageDomain(workspace, pageId, domains).domain;
84
+ if (!domain)
85
+ return undefined;
86
+ const id = domainCredentialId(domain, NOTION_PROVIDER, NOTION_CHANNEL_CREDENTIAL);
87
+ return id === NOTION_CHANNEL_CREDENTIAL ? undefined : id;
88
+ }
89
+ /**
90
+ * {@link notionChannelCredentialFor} for a session's own surface key (`notion/<workspace>/<page>`) โ€”
91
+ * the form `resolvePartition(ctx).surfaceKey` produces.
92
+ *
93
+ * The read tools resolve the CALLER'S surface, not the page argument. That is deliberate and it is
94
+ * not a weaker check: `notion-scope.ts` has already established that an admitted read is either the
95
+ * caller's own page or a page in the caller's own domain, so the two resolve to the same domain by
96
+ * construction โ€” and the caller's surface is known BEFORE the fence runs, which the block-resolution
97
+ * walk needs (it must make a Notion call to find a block's owning page).
98
+ */
99
+ export function notionChannelCredentialForSurface(surfaceKey, domains) {
100
+ const parts = surfaceKey.split("/");
101
+ if (parts.length !== 3 || parts[0] !== "notion")
102
+ return undefined;
103
+ return notionChannelCredentialFor(parts[1] || undefined, parts[2] || undefined, domains);
104
+ }
105
+ /**
106
+ * The `credential_id` auth attribute a DELIVERY stowed on this session (notion-identity.ts
107
+ * buildNotionAuth), or undefined when there is none โ€” a plain, LOCAL structural read of
108
+ * `ctx.session.auth`, the same shape notion-identity.ts's own `notionAuthOf`/`notionAttr` probes
109
+ * read (kept as a separate three-line copy rather than an import for the same reason theirs is not a
110
+ * barrel port: a mapper/credential decision reads its own platform's attributes itself). `current` is
111
+ * this turn's auth; `initiator` is the fallback for a context that only carries the session's
112
+ * original one.
113
+ */
114
+ function notionDeliveryCredential(ctx) {
115
+ const auth = ctx.session?.auth?.current ?? ctx.session?.auth?.initiator;
116
+ const v = auth?.attributes?.credential_id;
117
+ if (typeof v === "string" && v.length > 0)
118
+ return v;
119
+ if (Array.isArray(v) && typeof v[0] === "string" && v[0].length > 0)
120
+ return v[0];
121
+ return undefined;
122
+ }
123
+ /**
124
+ * The credential a READ TOOL runs under (OPE-499). **The delivery's own carried credential wins**
125
+ * when this session was woken by one โ€” the same `credential_id` auth attribute the reply arm already
126
+ * reads (notion-post-arm.ts) โ€” so a session only ever reads what the integration that verified its
127
+ * delivery was actually shared into, never a wider domain binding a double-shared page could reach.
128
+ *
129
+ * Falls back to the CALLING SESSION's own domain binding when no delivery credential rides the
130
+ * context โ€” an agent-initiated session (a routine, a proactive post) never carried one, and that is
131
+ * the pre-OPE-499 rule, unchanged. Fail-soft throughout: a synthetic or identity-less ctx has no
132
+ * partition to read, so it resolves undefined and the caller falls back to the org default, exactly
133
+ * like an unbound domain. That is the correct direction here: the DOMAIN FENCE (notion-scope.ts) is
134
+ * what refuses an out-of-scope read, and it runs whether or not a credential was selected.
135
+ */
136
+ export function notionCallerCredential(ctx, domains) {
137
+ const delivered = notionDeliveryCredential(ctx);
138
+ if (delivered)
139
+ return delivered;
140
+ try {
141
+ return notionChannelCredentialForSurface(resolvePartition(ctx).surfaceKey, domains);
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ }
147
+ /**
148
+ * Every Notion integration this deployment is reachable by โ€” the ORG DEFAULT FIRST, then one per
149
+ * domain that binds its own. The ingress tries each entry's inbound secret in this order.
150
+ */
151
+ export function notionIntegrations(domains) {
152
+ return connectorIntegrations(domains, NOTION_PROVIDER, NOTION_CHANNEL_CREDENTIAL, NOTION_WEBHOOK_SECRET_NAME);
153
+ }
154
+ export function notionSignerServesDomain(owner, signerCredentialId, integrations) {
155
+ const expected = domainCredentialId(owner, NOTION_PROVIDER, NOTION_CHANNEL_CREDENTIAL);
156
+ if (expected === signerCredentialId)
157
+ return { served: true, via: "credential", expected };
158
+ const secretOf = (credentialId) => integrations.find((i) => i.credentialId === credentialId)?.inboundSecret;
159
+ const signerSecret = secretOf(signerCredentialId);
160
+ if (signerSecret !== undefined && secretOf(expected) === signerSecret)
161
+ return { served: true, via: "shared-secret", expected };
162
+ return { served: false, expected };
163
+ }
164
+ /**
165
+ * Every domain a COMMENT delivery reaches THROUGH ITS WORKSPACE CLAIM โ€” the domains the payload's
166
+ * `workspace_id` can steer, and therefore the only ones {@link notionDeliveryScopeVerdict} fences.
167
+ *
168
+ * ๐Ÿ”ด TWO RESOLVERS ANSWER "WHICH DOMAIN?" DOWNSTREAM, AND THEY DO NOT ALWAYS AGREE. Both are
169
+ * consulted on the session the delivery opens, so a check on one alone would leave the other open:
170
+ *
171
+ * โ€ข {@link notionPageDomain} โ€” the credential binding (agent-initiated work) and the read fence
172
+ * (notion-scope.ts). Its roster leg compares the PAGE-ID segment alone.
173
+ * โ€ข `resolveDomainForChannel` โ€” the ambient gate, the engagement tier, the supervisor grain and
174
+ * the external-author policy, all keyed on the `notion/<workspace>/<page>` surface key. Its
175
+ * roster leg compares the WHOLE key, so a workspace that does not match the `reach` entry
176
+ * falls THROUGH to the claim leg.
177
+ *
178
+ * ๐Ÿ”ด WHY THE ROSTER LEGS ARE EXEMPT, AND THIS IS THE WHOLE DESIGN. A roster hit is not steerable by
179
+ * the payload's workspace:
180
+ * โ€ข {@link notionPageDomain}'s roster leg ignores the workspace outright, and OPE-496 established
181
+ * the page id from the comment Notion returned under the verified credential. Authenticated fact
182
+ * in, config match out.
183
+ * โ€ข `resolveDomainForChannel`'s roster leg needs the WHOLE key to appear in `reach`, so the
184
+ * workspace it accepts is one the deployer typed themselves.
185
+ * A claim leg is the opposite: a container entry names a workspace, the payload names a workspace, and a
186
+ * match hands over every page under it.
187
+ *
188
+ * KEEPING THE ROSTER LEGS OUT IS LOAD-BEARING, not a shortcut. A page shared with two integrations
189
+ * produces a delivery on EACH subscription, and each one must run under its own verifying integration
190
+ * (the header's "prefer, don't refuse" rule, and the escalation OPE-499 closes by carrying the
191
+ * delivery's credential into the reads). Fencing a rostered page on the signer would refuse the
192
+ * second subscription's delivery outright and delete that behaviour.
193
+ *
194
+ * Pure โ€” `domains` is injected, exactly like {@link notionPageDomain}'s.
195
+ */
196
+ export function notionDeliveryClaimedDomains(workspaceId, pageId, domains) {
197
+ const out = [];
198
+ const add = (d) => {
199
+ if (d && !out.some((seen) => seen.name === d.name))
200
+ out.push(d);
201
+ };
202
+ const page = notionPageDomain(workspaceId, pageId, domains);
203
+ if (page.via === "container")
204
+ add(page.domain);
205
+ const channel = resolveDomainForChannel(notionSurfaceKey(workspaceId, pageId), domains);
206
+ if (channel.via.startsWith("container:"))
207
+ add(channel.domain);
208
+ return out;
209
+ }
210
+ /**
211
+ * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
212
+ * ๐Ÿ”ด THE COMMENT-PLANE INGRESS FENCE (OPE-502) โ€” MAY THIS SIGNER OPEN A SESSION ON THIS PAGE?
213
+ * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
214
+ *
215
+ * `notion-webhook.ts` calls this after the page is established (OPE-496) and BEFORE the surface key
216
+ * decides anything. The rule is the trigger plane's rule, on the plane next door:
217
+ *
218
+ * A delivery may open a session on a domain it reaches THROUGH ITS WORKSPACE CLAIM only when that
219
+ * domain is one the SIGNING integration serves.
220
+ *
221
+ * WHY THE WORKSPACE NEEDS THIS AND THE PAGE DOES NOT. OPE-496 established the PAGE from the comment
222
+ * Notion returned under the verified credential, so the page is an authenticated fact. It left the
223
+ * WORKSPACE where it found it โ€” `ev.workspace_id`, a payload field nothing authenticates โ€” and the
224
+ * two are composed into `notion/<workspace>/<page>`. There is no verified source for the second
225
+ * half: Notion's REST surface returns no workspace id on any object (`/v1/users/me` gives
226
+ * `bot.workspace_name`, not an id), so no fetch under any credential can confirm the claim. Pinning
227
+ * the workspace to a config constant is wrong for a different reason โ€” one deployment may hold
228
+ * several workspaces. So the claim is fenced instead of proved, exactly as OPE-500 fenced the
229
+ * trigger plane's scope path.
230
+ *
231
+ * WORKED THROUGH, because the point is what it REFUSES. Domain `security` claims workspace
232
+ * `notion/W-sec` in a container entry and binds integration `notion-security` with its own inbound
233
+ * secret. A holder of domain `payments`'s `verification_token` comments on a page their OWN
234
+ * integration can see, then signs a delivery whose `workspace_id` reads `W-sec`. The page reaches
235
+ * `security` through that claim, so without this fence the session opens on `security` โ€” with
236
+ * `security`'s ambient flag, `security`'s read scope over every page under the claim, and (for an
237
+ * agent-initiated call) `security`'s credential. The fence reads that domain's credential, finds
238
+ * `notion-security` against a `notion-payments` signature, and refuses.
239
+ *
240
+ * FAIL CLOSED, AND THE REFUSALS ARE REAL:
241
+ * โ€ข NO SIGNER โ‡’ refuse. `webhooks.allowUnverifiedNotion` accepts a delivery with no secret
242
+ * configured anywhere, so nothing identifies a sender and the fence has no fact to read. A fence
243
+ * that says yes when it cannot tell is not a fence. The consequence is stated in the runbook: with
244
+ * that dev hatch on, a CLAIMED page opens no session. A rostered page is unaffected, so the local
245
+ * exercise the hatch exists for still runs.
246
+ * โ€ข NO CLAIMED DOMAIN โ‡’ allow. Either the page is rostered โ€” see
247
+ * {@link notionDeliveryClaimedDomains} for why a roster hit is not steerable โ€” or it is in no
248
+ * domain at all, where the `org` pseudo-domain answers and no payload field can steer that
249
+ * either.
250
+ *
251
+ * THE PARTITION IS DECIDED SEPARATELY (OPE-254 ยง2, folding OPE-537). This fence decides the domain
252
+ * BOUNDARY; which partition a delivery lands in is notion-webhook.ts's step 8c, which composes the
253
+ * key from the workspace segment of the `reach` entry that MATCHED โ€” `notionReachWorkspace`, on both
254
+ * legs. A page named outright therefore keys on config's workspace whatever a delivery claims. A
255
+ * page reached by a CONTAINER only matches when the payload's workspace equals the container's own
256
+ * text, so there the derived value equals the payload's by construction; a delivery naming a
257
+ * workspace no domain reaches matches nothing, and its key belongs to no domain โ€” no memory, no
258
+ * bundle, and the ambient gate refuses it โ€” rather than opening a second partition inside one.
259
+ *
260
+ * ๐Ÿ”ด WHY THE CONTAINER LEG NEEDED A CONFIG RULE, NOT JUST THE SAME READ. The exact-key match is
261
+ * workspace-BLIND โ€” it finds a page by id alone, so the workspace read back off the matched entry
262
+ * is a signal INDEPENDENT of whatever the payload said. The container leg is the opposite shape:
263
+ * `notionPageDomain`'s container match composes `notion/<payload-workspace>/<page>` and only admits
264
+ * it when a container entry's tenant segment equals THAT SAME payload workspace verbatim
265
+ * (surface-key-grammar.ts refuses a glob on the tenant segment, so the compare is always exact). So
266
+ * whenever a container admits a delivery, the matched entry's workspace text and the payload's are,
267
+ * by construction, the identical string โ€” reading the entry back is a no-op IN VALUE.
268
+ *
269
+ * What it is not is a no-op in PROPERTY, and that is the point. The remaining way one page could
270
+ * still key two ways was a domain reaching MORE THAN ONE Notion workspace: two honestly-signed
271
+ * deliveries for the same page, one naming each, opened two partitions. memory/domain.ts's
272
+ * `partitionTenantProblems` now refuses that config at load, so the matched entry names exactly one
273
+ * workspace per domain and the partition is a fact about config rather than about a delivery.
274
+ * No live re-keying follows: on every configuration that still loads, the derived value equals the
275
+ * value the payload carried, so no existing `channel_session` or memory row changes key.
276
+ *
277
+ * Pure โ€” `domains` is injected, so the whole decision is unit-testable with no config and no store.
278
+ */
279
+ export function notionDeliveryScopeVerdict(workspaceId, pageId, signerCredentialId, domains) {
280
+ const owners = notionDeliveryClaimedDomains(workspaceId, pageId, domains);
281
+ if (owners.length === 0)
282
+ return { ok: true, detail: "no domain reaches this page through a workspace claim" };
283
+ if (!signerCredentialId) {
284
+ return {
285
+ ok: false,
286
+ reason: `no integration's secret identified this delivery, and page ${quoteNotionId(pageId)} is claimed through workspace ${quoteNotionId(workspaceId)}`,
287
+ };
288
+ }
289
+ const integrations = notionIntegrations(domains);
290
+ let viaSharedSecret = false;
291
+ for (const owner of owners) {
292
+ const verdict = notionSignerServesDomain(owner, signerCredentialId, integrations);
293
+ if (!verdict.served) {
294
+ return {
295
+ ok: false,
296
+ reason: `page ${quoteNotionId(pageId)} reaches domain ${quoteNotionId(owner.name)} through its claim on workspace ` +
297
+ `${quoteNotionId(workspaceId)}, and that domain is served by notion credential ${quoteNotionId(verdict.expected)} โ€” ` +
298
+ `this delivery was signed by ${quoteNotionId(signerCredentialId)}. A signed body cannot open a session on another domain.`,
299
+ };
300
+ }
301
+ if (verdict.via === "shared-secret")
302
+ viaSharedSecret = true;
303
+ }
304
+ const named = owners.map((d) => quoteNotionId(d.name)).join(", ");
305
+ return {
306
+ ok: true,
307
+ detail: viaSharedSecret
308
+ ? `domain ${named} is served by an integration that shares this deployment's inbound secret`
309
+ : `domain ${named} is served by ${quoteNotionId(signerCredentialId)}`,
310
+ };
311
+ }
312
+ /**
313
+ * One id, safe to put in a log line. A refusal names the workspace and the page the SENDER chose โ€”
314
+ * the very fields nothing authenticates โ€” so a raw newline in one would forge a second log line. The
315
+ * value is bounded and stripped of every control character before it is quoted.
316
+ *
317
+ * Notion ids are UUIDs, so 64 characters is far more than a real one needs and short enough that a
318
+ * padded id cannot push the rest of the line out of a truncated log. Same helper the trigger plane's
319
+ * fence keeps for the same reason (notion-event-normalizer.ts `quote`); the two are three lines each
320
+ * and live beside the refusal they protect rather than in a shared string module.
321
+ */
322
+ function quoteNotionId(id) {
323
+ if (id === undefined)
324
+ return '"(none)"';
325
+ // The class is written in \u escapes on purpose: a raw control byte in a source file is its own
326
+ // defect class in this tree (`lint:invisible-bytes`).
327
+ return JSON.stringify(id.slice(0, 64).replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, "?"));
328
+ }
329
+ /**
330
+ * One integration's INBOUND signing key by operator-secret name, or undefined โ€” and the ONE place
331
+ * that decides whether a failure is REPORTED.
332
+ *
333
+ * ๐Ÿ”ด THE FAILURE THAT MUST BE LOUD. "Nobody stored this name" is the ordinary state for an
334
+ * integration the operator never configured: it contributes no key, a delivery it would have signed
335
+ * simply fails the gate, and saying so on every POST would be noise. Every OTHER failure โ€” an
336
+ * unarmed `OE_CREDENTIAL_ENCRYPTION_KEY`, a store that will not open โ€” makes EVERY delivery on that
337
+ * integration 401 with nothing in the log to explain it. It is reported.
338
+ *
339
+ * The two are told apart by the ERROR CLASS ({@link OperatorSecretNotSetError}), never by the
340
+ * message: the unarmed-key message ALSO contains the words "is not set", so the substring test this
341
+ * replaces reported neither and a broken credential store went silent.
342
+ *
343
+ * `get` and `log` are injected so the decision is testable with no store and no eve boot; the route
344
+ * passes `getOperatorSecret` and its own channel logger. The report names the SECRET NAME and the
345
+ * reason, never a value. Both arms return undefined โ€” this function never throws into a webhook.
346
+ */
347
+ export async function resolveNotionInboundSecret(name, get, log) {
348
+ try {
349
+ const resolved = await get(name);
350
+ return resolved.value.length > 0 ? resolved.value : undefined;
351
+ }
352
+ catch (err) {
353
+ if (err instanceof OperatorSecretNotSetError)
354
+ return undefined;
355
+ const message = err instanceof Error ? err.message : String(err);
356
+ log(`inbound signing key "${name}" did not resolve โ€” deliveries signed with it will 401: ${message}`);
357
+ return undefined;
358
+ }
359
+ }
360
+ //# sourceMappingURL=notion-credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-credentials.js","sourceRoot":"","sources":["../../../agent/channels/notion-credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,219 @@
1
+ import { type EngagementRow, type Partition } from "@open-everywhere/core/agent/adapter-api.js";
2
+ import type { NotionVia } from "./notion-scope.js";
3
+ /** The facts one Notion delivery contributes to the seam โ€” everything the anchor and the partition
4
+ * need, and nothing about the comment's text. */
5
+ export interface NotionSeamFacts {
6
+ readonly workspaceId: string | undefined;
7
+ readonly pageId: string | undefined;
8
+ /** The acting Notion user, so the partition the seam keys is built from the delivery's identity. */
9
+ readonly principalId?: string;
10
+ /** The integration this delivery verified under โ€” carried for binding parity with the wake. */
11
+ readonly credentialId?: string;
12
+ }
13
+ /** The engagement-registry anchor for one Notion delivery, or `undefined` when either id is missing.
14
+ *
15
+ * ๐Ÿ”ด IT IS THE ONE PREDICATE THE MAPPER AND THE SEAM SHARE. `bindNotion` publishes exactly this as
16
+ * `engagementKey`, so `stand_down`'s mark and this seam's read address the same row. */
17
+ export declare function notionEngagementAnchor(f: NotionSeamFacts): string | undefined;
18
+ /** The partition one page's engagement row lives in โ€” `notion/<workspace>/<page>`, which is what
19
+ * `resolvePartition` gives a bound Notion session and therefore what `stand_down` writes under. It
20
+ * is built through the same `buildNotionAuth` the wake dispatches, so the seam can never key a row
21
+ * the turn would not. Null on an unresolvable identity โ€” the caller then keeps today's path. */
22
+ export declare function notionSeamPartition(f: NotionSeamFacts): Partition | null;
23
+ /**
24
+ * What the seam says about one comment the classifier did NOT call `direct`. Three outcomes, each
25
+ * naming what the CALLER owes:
26
+ *
27
+ * โ€ข `unanchored` โ€” the delivery names no page (or no resolvable partition), so there is no row to
28
+ * address and the caller keeps today's path exactly.
29
+ * โ€ข `deliver` โ€” a plain comment on an engaged page. The caller wakes a turn even where the page's
30
+ * domain has not opted into the ambient supervisor: somebody addressed the agent on THIS page.
31
+ * โ€ข `hold` โ€” the seam does not admit it. `stopped` says WHY, and it is the one field a caller must
32
+ * branch on: `stopped: true` means the agent was asked to stand down on this page, so the comment
33
+ * must be SUPPRESSED outright โ€” including where the ambient supervisor is enabled and would
34
+ * otherwise answer. `stopped: false` means the page simply is not engaged, which is not a
35
+ * suppression: the caller keeps today's path (the domain opt-in then decides).
36
+ */
37
+ export type NotionEngagementVerdict = {
38
+ readonly kind: "unanchored";
39
+ readonly reason: string;
40
+ } | {
41
+ readonly kind: "deliver";
42
+ readonly anchor: string;
43
+ readonly reason: string;
44
+ } | {
45
+ readonly kind: "hold";
46
+ readonly anchor: string;
47
+ readonly reason: string;
48
+ readonly stopped: boolean;
49
+ };
50
+ /** Injectable seams so the whole verdict is unit-tested without a store. */
51
+ export interface NotionEngagementDeps {
52
+ readonly getEngagement?: (p: Partition, anchor: string) => Promise<EngagementRow | null>;
53
+ readonly markEngaged?: (p: Partition, anchor: string, atIso: string) => Promise<void>;
54
+ /** The durable once-guard BOTH this module's guards take โ€” the engage mark
55
+ * ({@link markNotionEngaged}) and the wake ({@link firstNotionDispatchOfComment}). They key on
56
+ * different prefixes, so one injected stub serves both. Injected so a test can drive a
57
+ * redelivered comment without an operational store. */
58
+ readonly markEventSeen?: (org: string, key: string, atIso: string) => Promise<boolean>;
59
+ readonly now?: () => string;
60
+ }
61
+ /**
62
+ * Resolve the seam for one NON-ADDRESSED comment โ€” READ-ONLY. An addressed comment never reaches
63
+ * this function: it marks instead ({@link markNotionEngaged}), so exactly one caller writes the row.
64
+ *
65
+ * THE DECISION ITSELF IS CORE'S. `shouldDeliverReply` owns the disqualifier order and the reasons;
66
+ * this function only translates Notion facts into its input. Nothing here re-states the policy.
67
+ *
68
+ * FAIL-CLOSED on a store error: `getThreadEngagement` reads a failure as `null`, so an unreadable
69
+ * registry holds rather than delivers.
70
+ *
71
+ * `authorIsBot` is the caller's own loop-guard verdict. The classifier drops a self-authored or
72
+ * integration-authored comment before this runs, so it is `false` on every live call; it stays a
73
+ * parameter rather than a hard-coded literal so the guard is visible at the call site and a test can
74
+ * drive the other direction.
75
+ */
76
+ export declare function resolveNotionEngagement(f: NotionSeamFacts, authorIsBot: boolean, deps?: NotionEngagementDeps): Promise<NotionEngagementVerdict>;
77
+ /**
78
+ * Mark one page ENGAGED on an addressed (name-drop or structured-mention) comment. ONCE PER COMMENT.
79
+ *
80
+ * `participating: true` is the re-invitation: it promotes the row and CLEARS a previous stand-down,
81
+ * which is what "name the agent to bring it back" means here.
82
+ *
83
+ * ๐Ÿ”ด THE MARK IS DEDUPED, AND THE STOP IS WHY โ€” the same durable guard GitHub's and Linear's marks
84
+ * take. Clearing the flag is right for a person's second name-drop and WRONG for Notion redelivering
85
+ * the first one. THE ROUTE DOES NOT ALWAYS ACK, so the redelivery is reachable: `handleNotionDelivery`
86
+ * carries no top-level `catch`, and eve's channel dispatcher answers a throw with a 500 โ€” while this
87
+ * mark sits ABOVE `getOrCreateSession` and the wake, either of which can throw on a store outage. The
88
+ * sequence that costs is then: name-drop marks the page, the mint throws, Notion is told 500, somebody
89
+ * asks the agent to stand down, and Notion's retry of the original comment lifts the stop.
90
+ * `commentId` is the trigger comment's own id, so the guard keys on the thing that must happen once.
91
+ * DURABLE layer only, and it expires with the store's TTL โ€” a retry later than that window can still
92
+ * re-mark. FAIL-OPEN on a store error, the posture every dedupe in this repository takes.
93
+ *
94
+ * Best-effort throughout โ€” `markThreadEngaged` swallows a store failure, because engagement
95
+ * bookkeeping must never fail a turn. Returns the anchor it marked, or null when it marked nothing.
96
+ */
97
+ export declare function markNotionEngaged(f: NotionSeamFacts, commentId: string | undefined, deps?: NotionEngagementDeps): Promise<string | null>;
98
+ /**
99
+ * ONE WAKE PER COMMENT (OPE-1906). TRUE = this comment has not woken the agent inside the window,
100
+ * so the caller should dispatch. FALSE = the same comment id already did, so the caller must not.
101
+ *
102
+ * ๐Ÿ”ด THE OTHER HALF OF THE LOOP GUARD, AND THE CLASSIFIER CANNOT BE IT. `classifyNotionComment`
103
+ * reads one comment's author; a REDELIVERY of a person's comment carries the same author and the
104
+ * same text, so it classifies identically and dispatches again โ€” a second model turn, a second
105
+ * `notion.dispatched` row, and a second reply into the discussion. Nothing else on this road is
106
+ * keyed on the delivery, so this is the only place that can say "again".
107
+ *
108
+ * REDELIVERY IS REACHABLE, and {@link markNotionEngaged} above already says why: the route does not
109
+ * always ack. `handleNotionDelivery` carries no top-level `catch`, and eve's channel dispatcher
110
+ * answers a throw with a 500, so Notion retries. A page shared with two subscriptions delivers the
111
+ * same comment twice by design (OPE-467), and a vendor retry needs no failure of ours at all.
112
+ *
113
+ * ๐Ÿ”ด THE MARK IS TAKEN BEFORE THE WAKE, AND THAT TRADE IS DELIBERATE. Marking afterwards would let
114
+ * two concurrent deliveries of one comment both run a turn, which is the cost this guard exists to
115
+ * stop; `markEventSeen` is atomic across instances, so marking first stops them. The price is that
116
+ * a delivery whose dispatch THROWS is not retried inside the window โ€” the same price the engage
117
+ * mark one step above already pays, and the reason this guard sits as LATE as it can, below every
118
+ * cheaper refusal, so only a delivery that was really about to wake a turn ever spends a mark.
119
+ *
120
+ * ๐Ÿ”ด A SUITE THAT DRIVES `handleNotionDelivery` PAST THIS POINT MUST INJECT `markEventSeen`, or
121
+ * pin its own store path. The default is the DURABLE operational store, and an unpinned suite in
122
+ * this package shares one sqlite file (scripts/suite-store-isolation-lint.ts says why) โ€” so a suite
123
+ * that replays a fixed comment id writes a mark that survives the run, and its NEXT run reads
124
+ * `duplicate-delivery` where it expected `dispatched`. `lint:suite-store` cannot catch that: its
125
+ * reach test is a source scan, and such a suite names no store entry point of its own. Five suites
126
+ * inject the stub today.
127
+ *
128
+ * FAIL-OPEN, three ways: a store error, an unresolvable partition and a delivery with no comment id
129
+ * all answer TRUE, so an outage costs a duplicate turn rather than a deaf deployment. The try/catch
130
+ * covers the INJECTED edge too, not just the live default (the shape webhooks/replay-dedupe.ts and
131
+ * webhooks/reject-audit.ts take) โ€” which is how a test drives the fail-open arm without breaking a
132
+ * real store. `commentId` is the thing that must happen once, and a Notion comment id is globally
133
+ * unique, so the key needs no anchor.
134
+ */
135
+ export declare function firstNotionDispatchOfComment(f: NotionSeamFacts, commentId: string | undefined, deps?: NotionEngagementDeps): Promise<boolean>;
136
+ /** The two addressing values a Notion delivery can carry into a dispatch โ€” `drop` never reaches one. */
137
+ export type NotionDispatchAddressing = "direct" | "channel-watch";
138
+ /**
139
+ * WHAT WOKE ONE ADMITTED DELIVERY, in the wake envelope's own vocabulary (core's `WakeAddressing`).
140
+ * It is the value the `notion.dispatched` audit row reports, and NO PRODUCT CODE BRANCHES ON IT.
141
+ * Every reader is an EVIDENCE reader outside the product: the journey pack's `reportedThreadFollow`
142
+ * (packages/core/scripts/e2e/engaged-rows.ts) gates `J-ENGAGED-notion`'s follow-up step on it,
143
+ * `drive-notion-namedrop.ts` prints and asserts it, and the test tiers read it. So changing the
144
+ * value moves what the pack can prove; it moves no delivery decision.
145
+ *
146
+ * ๐Ÿ”ด IT IS DELIBERATELY NOT THE CLASSIFIER'S VALUE. The classifier answers one question โ€” did this
147
+ * comment ADDRESS the agent โ€” and an engaged follow-up did not, so it stays `channel-watch`. The
148
+ * supervisor core needs that: `channel-watch` is what makes the turn a JUDGED one, and promoting it
149
+ * would silence the very wake this seam exists to admit.
150
+ *
151
+ * ๐Ÿ”ด BUT `channel-watch` ALSO NAMES UNOWNED ROOM CHATTER on an ambient page, and the audit row is
152
+ * the one place those two must not read alike. A row that reported `channel-watch` for both cannot
153
+ * tell an ENGAGED wake from an AMBIENT one, and that distinction is the whole evidentiary worth of
154
+ * the ยง2.5 seam on this surface: it is what `J-ENGAGED-notion` gates its follow-up step on
155
+ * (`reportedThreadFollow`, packages/core/scripts/e2e/engaged-rows.ts). Until OPE-1501 the row was
156
+ * unreachable on an addressed-only page โ€” the ambient gate returned before the classifier ran โ€” so
157
+ * the collision cost nothing. The gate now admits an engaged follow-up, and the collision costs the
158
+ * proof, which is what OPE-1510 fixed here.
159
+ *
160
+ * ๐Ÿ”ด IT REPORTS THE REGISTRY, NOT THE DOOR. The seam is read for every plain comment, above and
161
+ * independently of the ambient gate, so on an ambient-ENABLED page a plain comment on an engaged
162
+ * page also reads `thread-follow` โ€” the ambient flag would have admitted it either way. The value
163
+ * therefore separates an engaged wake from an ambient one only where the domain has ambient OFF,
164
+ * which is the addressed-only posture the journeys drive.
165
+ *
166
+ * `engagedFollowUp` is the webhook core's own seam verdict, and the `channel-watch` guard below is
167
+ * belt-and-braces: an ADDRESSED comment never asks the seam, so it can never carry the flag.
168
+ */
169
+ export declare function notionWakeAddressing(addressing: NotionDispatchAddressing, engagedFollowUp: boolean | undefined): "direct" | "channel-watch" | "thread-follow";
170
+ /** The `notion.dispatched` row's two seam fields: what woke the delivery, and how the operator's
171
+ * config holds the page. */
172
+ export interface NotionWakeAuditDetail {
173
+ readonly addressing: "direct" | "channel-watch" | "thread-follow";
174
+ readonly via: NotionVia;
175
+ }
176
+ /**
177
+ * THE SEAM'S WHOLE CONTRIBUTION TO THE AUDIT ROW, as one pure mapping.
178
+ *
179
+ * ๐Ÿ”ด THE TWO FIELDS ANSWER DIFFERENT QUESTIONS, AND A ROW NEEDS BOTH. `addressing` says WHAT WOKE
180
+ * the delivery. `via` says HOW THE OPERATOR DECLARED THE PAGE, which is the domain origin the
181
+ * addressed door gated on (`notionPageDomain`, channels/notion-scope.ts). A `container` claim
182
+ * reaches pages NOBODY ENUMERATED โ€” the whole workspace, or a same-level page glob, which are the
183
+ * two shapes a container entry may take (memory/domain.ts); a `roster` entry is a page an
184
+ * operator wrote down. Both reach the same door, and a row carrying only `addressing` reports them
185
+ * identically โ€” which leaves the wider admission looking exactly like the narrower one in the
186
+ * audit trail. The row says WHICH KIND of claim, never how wide that claim is: read the domain's
187
+ * own container entry for the breadth.
188
+ *
189
+ * `via` is the webhook core's own read, carried on the dispatch. An absent field reads `none`: the
190
+ * conservative word, so a caller that omits it can never claim a declaration it did not make.
191
+ */
192
+ export declare function notionWakeAuditDetail(addressing: NotionDispatchAddressing, engagedFollowUp: boolean | undefined, via: NotionVia | undefined): NotionWakeAuditDetail;
193
+ /** The three ids that let one Notion delivery be joined to the turn it woke. IDS ONLY. */
194
+ export interface NotionDispatchJoinIds {
195
+ /** The WEBHOOK ENVELOPE's own `id` โ€” unique to this delivery, so two deliveries of the SAME
196
+ * comment are tellable apart. Absent when the body carried no id. */
197
+ readonly deliveryId?: string;
198
+ /** The TRIGGER COMMENT's id โ€” what the delivery named, and what a replay leg configures. */
199
+ readonly commentId?: string;
200
+ /** The single-window wake this delivery minted โ€” the join key `wake.ingress` and
201
+ * `wake.turn_start` carry, and therefore the bridge to the session that answered. Absent on a
202
+ * per-doc (non-unified) delivery, which mints no window wake. */
203
+ readonly wakeId?: string;
204
+ }
205
+ /**
206
+ * Build the join ids for the row. Pure, and EVERY FIELD IS AN ID โ€” never a comment body, an author
207
+ * name or a page title.
208
+ *
209
+ * ๐Ÿ”ด WHY THE ROW NEEDS THEM. Before this, `notion.dispatched` recorded the discussion and the
210
+ * addressing but nothing that identified the DELIVERY, so a leg that forged one replay could only
211
+ * scan a time window for a verdict row and hope no other wake fell inside it. S-TAINT-DRYRUN Arm B
212
+ * did exactly that and the scan could not tell its own replay's verdict from an ambient one
213
+ * (OPE-1907). With the delivery id on the row, one forge joins one wake joins one session.
214
+ *
215
+ * An absent value is OMITTED rather than written as `undefined`: an audit row that carries a key
216
+ * with no value reads, to anyone querying it, like a value that was looked for and found empty.
217
+ */
218
+ export declare function notionDispatchJoinDetail(ids: NotionDispatchJoinIds): Record<string, string>;
219
+ //# sourceMappingURL=notion-engagement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-engagement.d.ts","sourceRoot":"","sources":["../../../agent/channels/notion-engagement.ts"],"names":[],"mappings":"AAiBA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,4CAA4C,CAAC;AAIpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD;kDACkD;AAClD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,oGAAoG;IACpG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,+FAA+F;IAC/F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;yFAGyF;AACzF,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAE7E;AAED;;;iGAGiG;AACjG,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI,CAaxE;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,uBAAuB,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3G,4EAA4E;AAC5E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACzF,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtF;;;4DAGwD;IACxD,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,uBAAuB,CAC3C,CAAC,EAAE,eAAe,EAClB,WAAW,EAAE,OAAO,EACpB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,uBAAuB,CAAC,CAiBlC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAclJ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,4BAA4B,CAChD,CAAC,EAAE,eAAe,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED,wGAAwG;AACxG,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,eAAe,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,wBAAwB,EACpC,eAAe,EAAE,OAAO,GAAG,SAAS,GACnC,QAAQ,GAAG,eAAe,GAAG,eAAe,CAE9C;AAED;6BAC6B;AAC7B,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,UAAU,EAAE,QAAQ,GAAG,eAAe,GAAG,eAAe,CAAC;IAClE,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,wBAAwB,EACpC,eAAe,EAAE,OAAO,GAAG,SAAS,EACpC,GAAG,EAAE,SAAS,GAAG,SAAS,GACzB,qBAAqB,CAEvB;AAyCD,0FAA0F;AAC1F,MAAM,WAAW,qBAAqB;IACpC;0EACsE;IACtE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;sEAEkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM3F"}