@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,119 @@
1
+ import type { SessionMode, Visibility, IdentityMapper } from "@open-everywhere/core/agent/adapter-api.js";
2
+ /** Structural match for eve's SessionAuthContext (attributes + principal). */
3
+ export interface OpenEverywhereNotionAuth {
4
+ readonly authenticator: string;
5
+ readonly principalId: string;
6
+ readonly principalType: string;
7
+ readonly attributes: Record<string, string>;
8
+ }
9
+ export interface NotionIdentityInput {
10
+ /** Notion workspace id — becomes the OpenEverywhere `workspace` (tenant tier). */
11
+ readonly workspaceId?: string;
12
+ /** The page the comment lives on — becomes the OpenEverywhere `surface` (the silo grain). */
13
+ readonly pageId?: string;
14
+ /** The discussion thread the comment belongs to — the post-back anchor (create-comment
15
+ * targets `discussion_id`), NOT the partition key. */
16
+ readonly discussionId?: string;
17
+ /** The specific comment that triggered this turn — for audit/provenance. */
18
+ readonly commentId?: string;
19
+ /** The acting Notion user (comment author), if known — becomes the principal. */
20
+ readonly principalId?: string;
21
+ /**
22
+ * The Notion channel credential this DELIVERY was selected to run under (OPE-467) — the
23
+ * integration whose secret verified it. Stowed so every reply for this turn posts under the SAME
24
+ * integration that fetched the comment. Without it a reply re-derives the credential from the
25
+ * page's domain binding alone, and a page shared with an integration its domain does not bind
26
+ * would fetch under one key and answer under another whose integration was never shared the page
27
+ * — `createCommentReply` throws and the agent silently never answers.
28
+ *
29
+ * HOST-SET (§AUTH-ATTRIBUTE PROVENANCE): built from the HMAC gate's verdict, never from the
30
+ * comment body, so it cannot be forged into naming another integration's credential.
31
+ */
32
+ readonly credentialId?: string;
33
+ /**
34
+ * The declared TRUST ROOT this page sits under, canonical form, when the ancestor walk matched one
35
+ * (OPE-492; channels/notion-ancestry-trust.ts resolveNotionDeliveryTrust). Present ⇒ the operator
36
+ * declared a page above this one internal, so {@link bindNotion} binds `internal` instead of the
37
+ * fail-closed `external`. Absent on every untrusted verdict, which is every verdict on a
38
+ * deployment that declares no roots.
39
+ *
40
+ * HOST-SET (§AUTH-ATTRIBUTE PROVENANCE): the webhook core resolves it from Notion's own parent
41
+ * chain under the VERIFIED credential, never from the comment body, so it cannot be forged into
42
+ * claiming a root.
43
+ */
44
+ readonly trustRoot?: string;
45
+ }
46
+ /** Build the auth context OpenEverywhere dispatches with — raw facts as string attributes. */
47
+ export declare function buildNotionAuth(input: NotionIdentityInput): OpenEverywhereNotionAuth;
48
+ /**
49
+ * The workspaceKey a Notion surface resolves to (`notion/<workspace id>`) — the value an
50
+ * operator lists in agent/identity/surfaces.ts to vouch a whole Notion workspace
51
+ * as internal (which lets its pages' learnings flow to shared memory despite the
52
+ * fail-closed default below). Takes the raw workspace id as Notion reports it on the
53
+ * webhook, so the operator pastes what they see. Notion ids are UUIDs, already within the
54
+ * binding's `seg` charset (hyphens allowed), so no encoding is needed. notion-test.ts pins
55
+ * this key against the one bindFrom actually produces.
56
+ */
57
+ export declare function notionWorkspaceKey(workspaceId: string): string;
58
+ /**
59
+ * The fully-qualified surfaceKey a Notion page resolves to — `notion/<workspace>/<page>` — the
60
+ * SAME shape resolvePartition(ctx) produces for a bound Notion session (memory/partition.ts:
61
+ * `platform/workspace/surface`, surface = the page). The ambient-supervisor gate + the
62
+ * channel_session registry are keyed by this, so the webhook core (which runs BEFORE any eve
63
+ * binding) must compute the identical key. Falls back through discussion → "unknown" the same way
64
+ * bindNotion picks its surface, so an under-identified event still keys to a stable string.
65
+ */
66
+ export declare function notionSurfaceKey(workspaceId: string | undefined, surface: string | undefined): string;
67
+ /**
68
+ * The CONVERSATION key the stop registry keys a Notion turn by (OPE-989) — `<workspace>/<page>`.
69
+ * ONE derivation, used by BOTH sides of the affordance: {@link bindNotion} writes it onto the
70
+ * anchor row at turn start (as `anchorChannelId`), and the stop leg (channels/notion-stop.ts)
71
+ * rebuilds it from the fetched comment. Two spellings of this key would make the arm silently miss
72
+ * — the same discipline githubConversationKey documents.
73
+ *
74
+ * THE PAGE, NOT THE DISCUSSION. The Notion doc-session is page-scoped (notion-supervisor.ts's
75
+ * `notionSupervisorToken` keys the standing session by page, not by discussion — "a page-scoped
76
+ * standing session aware of all the doc's discussions"), so the conversation a stop trigger can
77
+ * name is the page, mirroring how a GitHub stop names the issue/PR and a Teams stop names the
78
+ * reply chain.
79
+ *
80
+ * Returns undefined on any missing part — never "unknown" (unlike {@link notionSurfaceKey}, which
81
+ * exists for a different purpose: a STABLE partition key even under-identified). A partial anchor
82
+ * key would let two different under-identified deliveries collide and cancel each other's turns.
83
+ */
84
+ export declare function notionConversationKey(workspaceId: string | undefined, pageId: string | undefined): string | undefined;
85
+ /**
86
+ * The inverse of {@link notionConversationKey} — split the `<workspace>/<page>` string back into its
87
+ * two Notion ids. Undefined on anything that is not exactly two non-empty segments: a malformed or
88
+ * partial address must never resolve to a guessed page. Notion ids are UUIDs (no `/`), so splitting
89
+ * on the first slash is exact — there is no ambiguity to resolve.
90
+ *
91
+ * The one caller today is `notion-message-ops.ts` (OPE-1043): `SurfaceMessageOps.postText`/
92
+ * `editMessage` are handed only the composed `channelId` (this key), never the two ids apart, so a
93
+ * transport that needs the page or the workspace on its own has to parse it back out. Kept beside
94
+ * the composer rather than in that file, for the same reason the composer's own doc states: one
95
+ * derivation, read by both sides, so the write and the read can never drift into two spellings.
96
+ */
97
+ export declare function parseNotionConversationKey(channelId: string): {
98
+ readonly workspaceId: string;
99
+ readonly pageId: string;
100
+ } | undefined;
101
+ /**
102
+ * Notion has NO 1:1 DM primitive — every comment is on a shared page/discussion — so a
103
+ * Notion surface is always `channel` mode. Total function (no input) kept for symmetry
104
+ * with the other platforms' mode helpers and so the mapper reads the same shape.
105
+ */
106
+ export declare function notionMode(): SessionMode;
107
+ /**
108
+ * Notion facts → visibility. FAIL CLOSED — Notion is ALWAYS private.
109
+ * An internal integration only sees pages EXPLICITLY shared with it, and the webhook
110
+ * carries no cheap, reliable "this page is workspace-public / world-visible" bit — the
111
+ * `accessible_by` array is a coarse who-can-see list, not a public/private classifier.
112
+ * With no positive signal that a page is a broadly-shared internal space, a surface's
113
+ * learnings must never flow up to a shared (workspace) tier: every Notion page stays
114
+ * surface-tier private. A future maintainer who finds a positive workspace-visible signal
115
+ * (or enriches page permissions from the API) flips this — until then it fails closed.
116
+ */
117
+ export declare function notionVisibility(): Visibility;
118
+ export declare const bindNotion: IdentityMapper;
119
+ //# sourceMappingURL=notion-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-identity.d.ts","sourceRoot":"","sources":["../../../agent/channels/notion-identity.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAmB,cAAc,EAAe,MAAM,4CAA4C,CAAC;AAExI,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,mBAAmB;IAClC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;2DACuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,wBAAwB,CAcpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAErG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAErH;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAMnI;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,WAAW,CAExC;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,IAAI,UAAU,CAE7C;AAkCD,eAAO,MAAM,UAAU,EAAE,cA8DxB,CAAC"}
@@ -0,0 +1,205 @@
1
+ // Pure Notion→OpenEverywhere identity capture. The Notion channel (notion.ts) calls
2
+ // these to stow RAW Notion facts (workspace / page / discussion / comment ids + the
3
+ // commenting actor) into the session auth context's `attributes`. The INTERPRETATION of
4
+ // those facts into mode/visibility lives here too — notionMode/notionVisibility — so the
5
+ // test can call them and the security-critical "is this private?" decision sits in one
6
+ // tested place. This file imports no eve types (only a type-only alias, erased at
7
+ // compile), so it's trivially unit-testable.
8
+ //
9
+ // SILO CHOICE (why surface = the PAGE, not the workspace and not the comment thread):
10
+ // the surface-fit criterion is "the persistent multi-instance container, NOT the
11
+ // invocation point". Notion's tiers map onto ours exactly like Linear's do:
12
+ // Notion workspace ≈ Linear organization ≈ our `workspace` (the tenant tier)
13
+ // Notion PAGE ≈ Linear TEAM ≈ our `surface` (the silo — one page
14
+ // hosts many discussion threads over time,
15
+ // just as a team hosts many issues)
16
+ // Notion discussion ≈ Linear issue ≈ the conversation ANCHOR (post-back target
17
+ // + continuation key), NOT the partition key
18
+ // So a page's accumulated memory is reused across every comment thread on it, while a
19
+ // bare comment thread (which comes and goes) is only the reply anchor. This mirrors Slack
20
+ // too: surface = channel, the thread is the anchor. (A finer teamspace→page two-level silo
21
+ // is a future refinement; page is the launch grain, matching the Linear team grain.)
22
+ /** Build the auth context OpenEverywhere dispatches with — raw facts as string attributes. */
23
+ export function buildNotionAuth(input) {
24
+ const attributes = {};
25
+ if (input.workspaceId)
26
+ attributes.workspace_id = input.workspaceId;
27
+ if (input.pageId)
28
+ attributes.page_id = input.pageId;
29
+ if (input.discussionId)
30
+ attributes.discussion_id = input.discussionId;
31
+ if (input.commentId)
32
+ attributes.comment_id = input.commentId;
33
+ if (input.credentialId)
34
+ attributes.credential_id = input.credentialId;
35
+ if (input.trustRoot)
36
+ attributes.trust_root = input.trustRoot;
37
+ return {
38
+ authenticator: "notion",
39
+ principalId: input.principalId ?? "",
40
+ principalType: "notion-user",
41
+ attributes,
42
+ };
43
+ }
44
+ /**
45
+ * The workspaceKey a Notion surface resolves to (`notion/<workspace id>`) — the value an
46
+ * operator lists in agent/identity/surfaces.ts to vouch a whole Notion workspace
47
+ * as internal (which lets its pages' learnings flow to shared memory despite the
48
+ * fail-closed default below). Takes the raw workspace id as Notion reports it on the
49
+ * webhook, so the operator pastes what they see. Notion ids are UUIDs, already within the
50
+ * binding's `seg` charset (hyphens allowed), so no encoding is needed. notion-test.ts pins
51
+ * this key against the one bindFrom actually produces.
52
+ */
53
+ export function notionWorkspaceKey(workspaceId) {
54
+ return `notion/${workspaceId}`;
55
+ }
56
+ /**
57
+ * The fully-qualified surfaceKey a Notion page resolves to — `notion/<workspace>/<page>` — the
58
+ * SAME shape resolvePartition(ctx) produces for a bound Notion session (memory/partition.ts:
59
+ * `platform/workspace/surface`, surface = the page). The ambient-supervisor gate + the
60
+ * channel_session registry are keyed by this, so the webhook core (which runs BEFORE any eve
61
+ * binding) must compute the identical key. Falls back through discussion → "unknown" the same way
62
+ * bindNotion picks its surface, so an under-identified event still keys to a stable string.
63
+ */
64
+ export function notionSurfaceKey(workspaceId, surface) {
65
+ return `notion/${workspaceId ?? "unknown"}/${surface ?? "unknown"}`;
66
+ }
67
+ /**
68
+ * The CONVERSATION key the stop registry keys a Notion turn by (OPE-989) — `<workspace>/<page>`.
69
+ * ONE derivation, used by BOTH sides of the affordance: {@link bindNotion} writes it onto the
70
+ * anchor row at turn start (as `anchorChannelId`), and the stop leg (channels/notion-stop.ts)
71
+ * rebuilds it from the fetched comment. Two spellings of this key would make the arm silently miss
72
+ * — the same discipline githubConversationKey documents.
73
+ *
74
+ * THE PAGE, NOT THE DISCUSSION. The Notion doc-session is page-scoped (notion-supervisor.ts's
75
+ * `notionSupervisorToken` keys the standing session by page, not by discussion — "a page-scoped
76
+ * standing session aware of all the doc's discussions"), so the conversation a stop trigger can
77
+ * name is the page, mirroring how a GitHub stop names the issue/PR and a Teams stop names the
78
+ * reply chain.
79
+ *
80
+ * Returns undefined on any missing part — never "unknown" (unlike {@link notionSurfaceKey}, which
81
+ * exists for a different purpose: a STABLE partition key even under-identified). A partial anchor
82
+ * key would let two different under-identified deliveries collide and cancel each other's turns.
83
+ */
84
+ export function notionConversationKey(workspaceId, pageId) {
85
+ return workspaceId && pageId ? `${workspaceId}/${pageId}` : undefined;
86
+ }
87
+ /**
88
+ * The inverse of {@link notionConversationKey} — split the `<workspace>/<page>` string back into its
89
+ * two Notion ids. Undefined on anything that is not exactly two non-empty segments: a malformed or
90
+ * partial address must never resolve to a guessed page. Notion ids are UUIDs (no `/`), so splitting
91
+ * on the first slash is exact — there is no ambiguity to resolve.
92
+ *
93
+ * The one caller today is `notion-message-ops.ts` (OPE-1043): `SurfaceMessageOps.postText`/
94
+ * `editMessage` are handed only the composed `channelId` (this key), never the two ids apart, so a
95
+ * transport that needs the page or the workspace on its own has to parse it back out. Kept beside
96
+ * the composer rather than in that file, for the same reason the composer's own doc states: one
97
+ * derivation, read by both sides, so the write and the read can never drift into two spellings.
98
+ */
99
+ export function parseNotionConversationKey(channelId) {
100
+ const i = channelId.indexOf("/");
101
+ if (i <= 0 || i === channelId.length - 1)
102
+ return undefined;
103
+ const workspaceId = channelId.slice(0, i);
104
+ const pageId = channelId.slice(i + 1);
105
+ return workspaceId.length > 0 && pageId.length > 0 ? { workspaceId, pageId } : undefined;
106
+ }
107
+ /**
108
+ * Notion has NO 1:1 DM primitive — every comment is on a shared page/discussion — so a
109
+ * Notion surface is always `channel` mode. Total function (no input) kept for symmetry
110
+ * with the other platforms' mode helpers and so the mapper reads the same shape.
111
+ */
112
+ export function notionMode() {
113
+ return "channel";
114
+ }
115
+ /**
116
+ * Notion facts → visibility. FAIL CLOSED — Notion is ALWAYS private.
117
+ * An internal integration only sees pages EXPLICITLY shared with it, and the webhook
118
+ * carries no cheap, reliable "this page is workspace-public / world-visible" bit — the
119
+ * `accessible_by` array is a coarse who-can-see list, not a public/private classifier.
120
+ * With no positive signal that a page is a broadly-shared internal space, a surface's
121
+ * learnings must never flow up to a shared (workspace) tier: every Notion page stays
122
+ * surface-tier private. A future maintainer who finds a positive workspace-visible signal
123
+ * (or enriches page permissions from the API) flips this — until then it fails closed.
124
+ */
125
+ export function notionVisibility() {
126
+ return "private";
127
+ }
128
+ function notionAuthOf(ctx) {
129
+ return ctx.session?.auth?.current ?? ctx.session?.auth?.initiator ?? null;
130
+ }
131
+ function notionAttr(a, keys) {
132
+ if (!a)
133
+ return undefined;
134
+ for (const k of keys) {
135
+ const v = a[k];
136
+ if (typeof v === "string" && v.length > 0)
137
+ return v;
138
+ if (Array.isArray(v) && typeof v[0] === "string" && v[0].length > 0)
139
+ return v[0];
140
+ }
141
+ return undefined;
142
+ }
143
+ export const bindNotion = (ctx) => {
144
+ const auth = notionAuthOf(ctx);
145
+ const a = auth?.attributes;
146
+ const workspaceId = notionAttr(a, ["workspace_id", "workspaceId"]);
147
+ // Surface = the PAGE (silo grain); fall back to the discussion, then the comment, so a
148
+ // session always keys to a stable surface even on an under-identified event.
149
+ const surface = notionAttr(a, ["page_id", "pageId"]) ??
150
+ notionAttr(a, ["discussion_id", "discussionId"]) ??
151
+ notionAttr(a, ["comment_id", "commentId"]);
152
+ const discussionId = notionAttr(a, ["discussion_id", "discussionId"]);
153
+ const pageId = notionAttr(a, ["page_id", "pageId"]);
154
+ // The delivery's selected integration (OPE-467), so `reply` posts under the credential that
155
+ // fetched. Absent on a session that predates the stamp ⇒ the reply arm falls back to the page's
156
+ // domain binding, which is the pre-OPE-467 behaviour.
157
+ const credentialId = notionAttr(a, ["credential_id", "credentialId"]);
158
+ // The ANCESTOR-WALK verdict the webhook core resolved for THIS delivery (OPE-492). Present only
159
+ // when the page sits under a declared trust root; the stamp names the root that matched.
160
+ const trustRoot = notionAttr(a, ["trust_root", "trustRoot"]);
161
+ const principal = auth?.principalId;
162
+ return {
163
+ workspace: workspaceId,
164
+ surface,
165
+ mode: notionMode(),
166
+ visibility: notionVisibility(),
167
+ // Notion refutes no author and reports no room-grain external signal, so trust here is ALWAYS an
168
+ // operator declaration — never a platform verdict. Two declarations reach it, and this is the
169
+ // ONE the platform mapper carries: the page sits under a declared TRUST ROOT, walked and matched
170
+ // at ingestion (channels/notion-ancestry-trust.ts). The other is the flat per-surface vouch
171
+ // (identity/surfaces.ts), which bindFrom applies one rung lower.
172
+ //
173
+ // POSITIVE-ONLY. No root matched ⇒ undefined, NEVER "external": bindFrom must still fall through
174
+ // to the per-surface vouch and then to the fail-closed default. Returning "external" here would
175
+ // out-rank the operator's own `defineSurface` line and silently switch off shared memory on every
176
+ // Notion page of a deployment that declares no roots.
177
+ trust: trustRoot ? "internal" : undefined,
178
+ principalId: (principal && principal.length > 0 ? principal : undefined) ?? notionAttr(a, ["user_id", "userId"]),
179
+ // Post-back targets the DISCUSSION thread (create-comment's discussion_id), with the page
180
+ // riding along. Not a thread ts — Notion replies key off discussion_id, carried on the
181
+ // session state the channel stashes, so this target is for routine/proactive use.
182
+ target: discussionId ? { discussionId, ...(pageId ? { pageId } : {}), ...(credentialId ? { credentialId } : {}) } : undefined,
183
+ // THE STOP REGISTRY'S ADDRESS FOR THIS TURN (OPE-989). A Notion doc-session is PAGE-scoped
184
+ // (notionSupervisorToken keys the standing session by page), so that is what a stop directive
185
+ // posted on the page can name. It cannot ride `target`: that object composes the REPLY target
186
+ // (the discussion), a different grain that changes on every comment.
187
+ //
188
+ // Notion pushes no reaction webhook (comment.created/updated/deleted is the whole comment event
189
+ // catalogue — developers.notion.com/reference/webhooks-events-delivery, read 2026-08-23), so the
190
+ // trigger on this surface is a bare `stop` comment read at the ingress — see
191
+ // channels/notion-stop.ts. The key it resolves by is this one.
192
+ ...(notionConversationKey(workspaceId, pageId) ? { anchorChannelId: notionConversationKey(workspaceId, pageId) } : {}),
193
+ // THE ENGAGEMENT REGISTRY'S ADDRESS FOR THIS TURN (OPE-1501) — the SAME page key, and
194
+ // deliberately the same derivation rather than a second spelling of it. `stand_down` marks
195
+ // `binding.engagementKey ?? binding.threadTs`, and Notion carries no `threadTs`, so without this
196
+ // field the verb found no anchor and refused on every Notion turn.
197
+ //
198
+ // THE PAGE, NOT THE DISCUSSION, for the reason {@link notionConversationKey} states: the doc
199
+ // session is page-scoped and aware of all the page's discussions, so a person asking the agent
200
+ // to stop is asking it to stop on the page. A per-discussion mark would leave the same standing
201
+ // session answering the discussion next door.
202
+ ...(notionConversationKey(workspaceId, pageId) ? { engagementKey: notionConversationKey(workspaceId, pageId) } : {}),
203
+ };
204
+ };
205
+ //# sourceMappingURL=notion-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-identity.js","sourceRoot":"","sources":["../../../agent/channels/notion-identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { type ClaimedIngressDelivery, type OperationalStore } from "@open-everywhere/core/agent/adapter-api.js";
2
+ import { type NotionIngressKey } from "./notion-ingress.js";
3
+ export declare const NOTION_INGRESS_DRAIN_PATH = "/eve/v1/notion/drain";
4
+ export declare const NOTION_INGRESS_DRAIN_SIGNATURE_HEADER = "x-oe-notion-drain-signature";
5
+ export declare const NOTION_COMMENT_DISPATCH_SOURCE = "notion-comment-dispatch";
6
+ export declare const NOTION_COMMENT_DISPATCH_AUTHORITY = "notion";
7
+ export interface NotionIngressDrainKey extends NotionIngressKey {
8
+ }
9
+ export interface AcceptedNotionIngressDrain {
10
+ readonly key: NotionIngressDrainKey;
11
+ readonly claim: ClaimedIngressDelivery;
12
+ readonly rawBody: string;
13
+ readonly signerCredentialId: string;
14
+ }
15
+ export type NotionIngressDisposition = "complete" | "discard" | "retry";
16
+ export interface NotionCommentDispatchGuard {
17
+ readonly claim: (commentId: string) => Promise<boolean>;
18
+ readonly complete: (commentId: string) => Promise<void>;
19
+ }
20
+ export interface AcceptNotionIngressDrainDeps {
21
+ readonly org: string;
22
+ readonly store: OperationalStore;
23
+ readonly secret?: string;
24
+ readonly now?: () => Date;
25
+ readonly claimId?: () => string;
26
+ }
27
+ export declare function signNotionIngressDrainBody(secret: string, body: string): string;
28
+ /** Cheap public-ACK precondition: recovery can authenticate and reach this deployment. */
29
+ export declare function notionIngressDrainAvailable(): boolean;
30
+ /** Authenticate an internal row-key request and atomically claim its exact org-scoped row. */
31
+ export declare function acceptNotionIngressDrainRequest(req: Request, deps: AcceptNotionIngressDrainDeps): Promise<{
32
+ readonly accepted: AcceptedNotionIngressDrain;
33
+ } | {
34
+ readonly response: Response;
35
+ }>;
36
+ /** Finish one primary claim according to the processor's explicit semantic result. */
37
+ export declare function settleNotionIngressDrain(accepted: AcceptedNotionIngressDrain, disposition: NotionIngressDisposition, deps: Pick<AcceptNotionIngressDrainDeps, "org" | "store" | "now">): Promise<void>;
38
+ /**
39
+ * A comment-level guard for the crash seam between "accepted by Eve" and primary-row completion.
40
+ * Its lease is deliberately the same length as the primary retry delay: a retry never mistakes an
41
+ * active pre-dispatch lease for a completed duplicate. A stale lease is stolen and may duplicate
42
+ * after an ambiguous post-dispatch crash, which is the honest at-least-once side of the choice.
43
+ */
44
+ export declare function createNotionCommentDispatchGuard(input: {
45
+ readonly org: string;
46
+ readonly store: OperationalStore;
47
+ readonly now?: () => Date;
48
+ readonly claimId?: () => string;
49
+ }): NotionCommentDispatchGuard;
50
+ export interface PostNotionIngressDrainDeps {
51
+ readonly secret?: string;
52
+ readonly baseUrl?: string;
53
+ readonly fetchImpl?: typeof fetch;
54
+ readonly now?: () => Date;
55
+ }
56
+ /** Post one content-free row key to this deployment's private drain route. */
57
+ export declare function postNotionIngressDrain(key: NotionIngressDrainKey, deps?: PostNotionIngressDrainDeps): Promise<void>;
58
+ /** One recovery tick. A failed self-hop leaves the row pending for the next minute. */
59
+ export declare function recoverNotionIngress(org: string, store: OperationalStore, deps?: PostNotionIngressDrainDeps & {
60
+ readonly now?: () => Date;
61
+ }): Promise<{
62
+ readonly due: number;
63
+ readonly posted: number;
64
+ readonly failed: number;
65
+ }>;
66
+ //# sourceMappingURL=notion-ingress-drain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-ingress-drain.d.ts","sourceRoot":"","sources":["../../../agent/channels/notion-ingress-drain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,185 @@
1
+ // DURABLE NOTION INGRESS DRAIN — the authenticated, fenced consumer of rows admitted by
2
+ // notion-ingress.ts. The public webhook stores a projected provider envelope and returns; both its
3
+ // immediate nudge and the minute recovery schedule send only the row key through this internal
4
+ // HMAC route. The payload never leaves the operational store.
5
+ import { createHmac, randomUUID } from "node:crypto";
6
+ import { getOrMintSelfSecret, readCappedBody, selfDispatchBaseUrl, selfSecretsArmed, verifyWebhookSignature, } from "@open-everywhere/core/agent/adapter-api.js";
7
+ import { decodeNotionIngressPayload, NOTION_INGRESS_SOURCE } from "./notion-ingress.js";
8
+ export const NOTION_INGRESS_DRAIN_PATH = "/eve/v1/notion/drain";
9
+ export const NOTION_INGRESS_DRAIN_SIGNATURE_HEADER = "x-oe-notion-drain-signature";
10
+ const NOTION_INGRESS_DRAIN_SECRET_ENV = "OE_NOTION_INGRESS_DRAIN_SECRET";
11
+ export const NOTION_COMMENT_DISPATCH_SOURCE = "notion-comment-dispatch";
12
+ // Comment ids are globally unique inside Notion. Keep this authority constant so the same comment
13
+ // arriving from two subscriptions shares ONE dispatch lease, matching the legacy OPE-1906 guard.
14
+ export const NOTION_COMMENT_DISPATCH_AUTHORITY = "notion";
15
+ const NOTION_INGRESS_LEASE_MS = 5 * 60_000;
16
+ const NOTION_INGRESS_RETRY_MS = NOTION_INGRESS_LEASE_MS + 1_000;
17
+ const NOTION_INGRESS_TOMBSTONE_MS = 24 * 60 * 60_000;
18
+ const NOTION_COMMENT_DISPATCH_TOMBSTONE_MS = 10 * 60_000;
19
+ const NOTION_INGRESS_DRAIN_MAX_BODY_BYTES = 16 * 1024;
20
+ const NOTION_INGRESS_DRAIN_MAX_SKEW_MS = 5 * 60_000;
21
+ const NOTION_INGRESS_DRAIN_BATCH = 25;
22
+ function nonEmpty(value, max = 512) {
23
+ return typeof value === "string" && value.length > 0 && value.length <= max ? value : undefined;
24
+ }
25
+ function parseEnvelope(value) {
26
+ if (!value || typeof value !== "object" || Array.isArray(value))
27
+ return undefined;
28
+ const row = value;
29
+ const authorityId = nonEmpty(row.authorityId);
30
+ const deliveryId = nonEmpty(row.deliveryId);
31
+ const issuedAt = row.issuedAt;
32
+ if (!authorityId || !deliveryId || typeof issuedAt !== "number" || !Number.isFinite(issuedAt))
33
+ return undefined;
34
+ return { authorityId, deliveryId, issuedAt };
35
+ }
36
+ export function signNotionIngressDrainBody(secret, body) {
37
+ return `sha256=${createHmac("sha256", secret).update(body, "utf8").digest("hex")}`;
38
+ }
39
+ async function drainSecret(injected) {
40
+ if (injected !== undefined)
41
+ return injected;
42
+ if (!selfSecretsArmed())
43
+ return undefined;
44
+ return getOrMintSelfSecret(NOTION_INGRESS_DRAIN_SECRET_ENV);
45
+ }
46
+ /** Cheap public-ACK precondition: recovery can authenticate and reach this deployment. */
47
+ export function notionIngressDrainAvailable() {
48
+ return selfSecretsArmed() && selfDispatchBaseUrl() !== undefined;
49
+ }
50
+ /** Authenticate an internal row-key request and atomically claim its exact org-scoped row. */
51
+ export async function acceptNotionIngressDrainRequest(req, deps) {
52
+ const read = await readCappedBody(req, NOTION_INGRESS_DRAIN_MAX_BODY_BYTES);
53
+ if ("tooLarge" in read)
54
+ return { response: new Response("payload too large", { status: 413 }) };
55
+ const secret = await drainSecret(deps.secret);
56
+ if (!secret)
57
+ return { response: new Response("not found", { status: 404 }) };
58
+ if (!verifyWebhookSignature(secret, read.body, req.headers.get(NOTION_INGRESS_DRAIN_SIGNATURE_HEADER))) {
59
+ return { response: new Response("unauthorized", { status: 401 }) };
60
+ }
61
+ let envelope;
62
+ try {
63
+ envelope = parseEnvelope(JSON.parse(read.body));
64
+ }
65
+ catch {
66
+ // handled below
67
+ }
68
+ if (!envelope)
69
+ return { response: new Response("bad request", { status: 400 }) };
70
+ const now = (deps.now ?? (() => new Date()))();
71
+ if (Math.abs(now.getTime() - envelope.issuedAt) > NOTION_INGRESS_DRAIN_MAX_SKEW_MS) {
72
+ return { response: new Response("unauthorized", { status: 401 }) };
73
+ }
74
+ const key = {
75
+ source: NOTION_INGRESS_SOURCE,
76
+ authorityId: envelope.authorityId,
77
+ deliveryId: envelope.deliveryId,
78
+ };
79
+ const staleBefore = new Date(now.getTime() - NOTION_INGRESS_LEASE_MS).toISOString();
80
+ const claim = await deps.store.claimIngressDelivery(deps.org, key, (deps.claimId ?? randomUUID)(), now.toISOString(), staleBefore);
81
+ // A duplicate nudge, active lease or completed tombstone is already safely represented in the
82
+ // store. Return 200 so the self-client does not amplify it; the minute scan handles stale work.
83
+ if (!claim)
84
+ return { response: new Response("ok", { status: 200 }) };
85
+ const payload = decodeNotionIngressPayload(claim.payloadJson);
86
+ if (!payload) {
87
+ await deps.store.releaseIngressDelivery(deps.org, key, claim.claimId, new Date(now.getTime() + NOTION_INGRESS_RETRY_MS).toISOString());
88
+ return { response: new Response("stored payload invalid", { status: 500 }) };
89
+ }
90
+ return {
91
+ accepted: {
92
+ key,
93
+ claim,
94
+ rawBody: JSON.stringify(payload.event),
95
+ signerCredentialId: key.authorityId,
96
+ },
97
+ };
98
+ }
99
+ /** Finish one primary claim according to the processor's explicit semantic result. */
100
+ export async function settleNotionIngressDrain(accepted, disposition, deps) {
101
+ const now = (deps.now ?? (() => new Date()))();
102
+ if (disposition === "discard") {
103
+ await deps.store.discardIngressDelivery(deps.org, accepted.key, accepted.claim.claimId);
104
+ return;
105
+ }
106
+ if (disposition === "retry") {
107
+ await deps.store.releaseIngressDelivery(deps.org, accepted.key, accepted.claim.claimId, new Date(now.getTime() + NOTION_INGRESS_RETRY_MS).toISOString());
108
+ return;
109
+ }
110
+ await deps.store.completeIngressDelivery(deps.org, accepted.key, accepted.claim.claimId, now.toISOString(), new Date(now.getTime() + NOTION_INGRESS_TOMBSTONE_MS).toISOString());
111
+ }
112
+ /**
113
+ * A comment-level guard for the crash seam between "accepted by Eve" and primary-row completion.
114
+ * Its lease is deliberately the same length as the primary retry delay: a retry never mistakes an
115
+ * active pre-dispatch lease for a completed duplicate. A stale lease is stolen and may duplicate
116
+ * after an ambiguous post-dispatch crash, which is the honest at-least-once side of the choice.
117
+ */
118
+ export function createNotionCommentDispatchGuard(input) {
119
+ let held;
120
+ return {
121
+ async claim(commentId) {
122
+ const now = (input.now ?? (() => new Date()))();
123
+ const key = {
124
+ source: NOTION_COMMENT_DISPATCH_SOURCE,
125
+ authorityId: NOTION_COMMENT_DISPATCH_AUTHORITY,
126
+ deliveryId: commentId,
127
+ };
128
+ await input.store.enqueueIngressDelivery(input.org, {
129
+ ...key,
130
+ payloadJson: JSON.stringify({ schemaVersion: 1 }),
131
+ receivedAt: now.toISOString(),
132
+ });
133
+ const claim = await input.store.claimIngressDelivery(input.org, key, (input.claimId ?? randomUUID)(), now.toISOString(), new Date(now.getTime() - NOTION_INGRESS_LEASE_MS).toISOString());
134
+ if (!claim)
135
+ return false;
136
+ held = { key, claimId: claim.claimId };
137
+ return true;
138
+ },
139
+ async complete(commentId) {
140
+ if (!held || held.key.deliveryId !== commentId)
141
+ throw new Error("Notion dispatch guard was not claimed");
142
+ const now = (input.now ?? (() => new Date()))();
143
+ const completed = await input.store.completeIngressDelivery(input.org, held.key, held.claimId, now.toISOString(), new Date(now.getTime() + NOTION_COMMENT_DISPATCH_TOMBSTONE_MS).toISOString());
144
+ if (!completed)
145
+ throw new Error("Notion dispatch guard completion lost its lease");
146
+ held = undefined;
147
+ },
148
+ };
149
+ }
150
+ /** Post one content-free row key to this deployment's private drain route. */
151
+ export async function postNotionIngressDrain(key, deps = {}) {
152
+ const base = deps.baseUrl ?? selfDispatchBaseUrl();
153
+ const secret = await drainSecret(deps.secret);
154
+ if (!base || !secret)
155
+ throw new Error("Notion ingress drain is unavailable");
156
+ const body = JSON.stringify({
157
+ authorityId: key.authorityId,
158
+ deliveryId: key.deliveryId,
159
+ issuedAt: (deps.now ?? (() => new Date()))().getTime(),
160
+ });
161
+ const response = await (deps.fetchImpl ?? fetch)(`${base}${NOTION_INGRESS_DRAIN_PATH}`, {
162
+ method: "POST",
163
+ headers: {
164
+ "content-type": "application/json",
165
+ [NOTION_INGRESS_DRAIN_SIGNATURE_HEADER]: signNotionIngressDrainBody(secret, body),
166
+ },
167
+ body,
168
+ signal: AbortSignal.timeout(20_000),
169
+ });
170
+ if (!response.ok)
171
+ throw new Error(`Notion ingress drain returned ${response.status}`);
172
+ }
173
+ /** One recovery tick. A failed self-hop leaves the row pending for the next minute. */
174
+ export async function recoverNotionIngress(org, store, deps = {}) {
175
+ const now = (deps.now ?? (() => new Date()))();
176
+ const due = await store.listDueIngressDeliveries(org, NOTION_INGRESS_SOURCE, now.toISOString(), new Date(now.getTime() - NOTION_INGRESS_LEASE_MS).toISOString(), NOTION_INGRESS_DRAIN_BATCH);
177
+ const results = await Promise.allSettled(due.map((key) => postNotionIngressDrain({
178
+ source: NOTION_INGRESS_SOURCE,
179
+ authorityId: key.authorityId,
180
+ deliveryId: key.deliveryId,
181
+ }, deps)));
182
+ const posted = results.filter((result) => result.status === "fulfilled").length;
183
+ return { due: due.length, posted, failed: due.length - posted };
184
+ }
185
+ //# sourceMappingURL=notion-ingress-drain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-ingress-drain.js","sourceRoot":"","sources":["../../../agent/channels/notion-ingress-drain.ts"],"names":[],"mappings":"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"}