@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.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +48 -0
- package/agent/channels/notion-acting-identity.ts +88 -0
- package/agent/channels/notion-ancestry-trust.ts +458 -0
- package/agent/channels/notion-api.ts +1290 -0
- package/agent/channels/notion-bot-user.ts +250 -0
- package/agent/channels/notion-catch-up.ts +339 -0
- package/agent/channels/notion-credentials.ts +417 -0
- package/agent/channels/notion-engagement.ts +372 -0
- package/agent/channels/notion-event-normalizer.ts +587 -0
- package/agent/channels/notion-identity.ts +271 -0
- package/agent/channels/notion-ingress-drain.ts +289 -0
- package/agent/channels/notion-ingress.ts +182 -0
- package/agent/channels/notion-message-ops.ts +140 -0
- package/agent/channels/notion-origin-codec.ts +38 -0
- package/agent/channels/notion-post-arm.ts +189 -0
- package/agent/channels/notion-question-return.ts +223 -0
- package/agent/channels/notion-read.ts +283 -0
- package/agent/channels/notion-scope.ts +560 -0
- package/agent/channels/notion-stop.ts +169 -0
- package/agent/channels/notion-supervisor.ts +856 -0
- package/agent/channels/notion-webhook-timing.ts +131 -0
- package/agent/channels/notion-webhook-verifier.ts +55 -0
- package/agent/channels/notion-webhook.ts +963 -0
- package/agent/channels/notion-work-search.ts +74 -0
- package/agent/channels/notion-work.ts +144 -0
- package/agent/channels/notion.ts +1242 -0
- package/agent/contributions.ts +39 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/memory/blocks.ts +295 -0
- package/agent/memory/memory.ts +622 -0
- package/agent/provider-events.ts +198 -0
- package/agent/register.ts +144 -0
- package/agent/schedules/notion-ingress.ts +20 -0
- package/agent/surface-parity.ts +105 -0
- package/agent/tools/notion_search.ts +110 -0
- package/agent/tools/read_notion_comments.ts +250 -0
- package/agent/tools/read_notion_page.ts +351 -0
- package/dist/agent/channels/notion-acting-identity.d.ts +23 -0
- package/dist/agent/channels/notion-acting-identity.d.ts.map +1 -0
- package/dist/agent/channels/notion-acting-identity.js +85 -0
- package/dist/agent/channels/notion-acting-identity.js.map +1 -0
- package/dist/agent/channels/notion-ancestry-trust.d.ts +208 -0
- package/dist/agent/channels/notion-ancestry-trust.d.ts.map +1 -0
- package/dist/agent/channels/notion-ancestry-trust.js +331 -0
- package/dist/agent/channels/notion-ancestry-trust.js.map +1 -0
- package/dist/agent/channels/notion-api.d.ts +655 -0
- package/dist/agent/channels/notion-api.d.ts.map +1 -0
- package/dist/agent/channels/notion-api.js +938 -0
- package/dist/agent/channels/notion-api.js.map +1 -0
- package/dist/agent/channels/notion-bot-user.d.ts +107 -0
- package/dist/agent/channels/notion-bot-user.d.ts.map +1 -0
- package/dist/agent/channels/notion-bot-user.js +192 -0
- package/dist/agent/channels/notion-bot-user.js.map +1 -0
- package/dist/agent/channels/notion-catch-up.d.ts +64 -0
- package/dist/agent/channels/notion-catch-up.d.ts.map +1 -0
- package/dist/agent/channels/notion-catch-up.js +289 -0
- package/dist/agent/channels/notion-catch-up.js.map +1 -0
- package/dist/agent/channels/notion-credentials.d.ts +210 -0
- package/dist/agent/channels/notion-credentials.d.ts.map +1 -0
- package/dist/agent/channels/notion-credentials.js +360 -0
- package/dist/agent/channels/notion-credentials.js.map +1 -0
- package/dist/agent/channels/notion-engagement.d.ts +219 -0
- package/dist/agent/channels/notion-engagement.d.ts.map +1 -0
- package/dist/agent/channels/notion-engagement.js +283 -0
- package/dist/agent/channels/notion-engagement.js.map +1 -0
- package/dist/agent/channels/notion-event-normalizer.d.ts +194 -0
- package/dist/agent/channels/notion-event-normalizer.d.ts.map +1 -0
- package/dist/agent/channels/notion-event-normalizer.js +542 -0
- package/dist/agent/channels/notion-event-normalizer.js.map +1 -0
- package/dist/agent/channels/notion-identity.d.ts +119 -0
- package/dist/agent/channels/notion-identity.d.ts.map +1 -0
- package/dist/agent/channels/notion-identity.js +205 -0
- package/dist/agent/channels/notion-identity.js.map +1 -0
- package/dist/agent/channels/notion-ingress-drain.d.ts +66 -0
- package/dist/agent/channels/notion-ingress-drain.d.ts.map +1 -0
- package/dist/agent/channels/notion-ingress-drain.js +185 -0
- package/dist/agent/channels/notion-ingress-drain.js.map +1 -0
- package/dist/agent/channels/notion-ingress.d.ts +64 -0
- package/dist/agent/channels/notion-ingress.d.ts.map +1 -0
- package/dist/agent/channels/notion-ingress.js +130 -0
- package/dist/agent/channels/notion-ingress.js.map +1 -0
- package/dist/agent/channels/notion-message-ops.d.ts +3 -0
- package/dist/agent/channels/notion-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/notion-message-ops.js +126 -0
- package/dist/agent/channels/notion-message-ops.js.map +1 -0
- package/dist/agent/channels/notion-origin-codec.d.ts +7 -0
- package/dist/agent/channels/notion-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/notion-origin-codec.js +32 -0
- package/dist/agent/channels/notion-origin-codec.js.map +1 -0
- package/dist/agent/channels/notion-post-arm.d.ts +3 -0
- package/dist/agent/channels/notion-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/notion-post-arm.js +185 -0
- package/dist/agent/channels/notion-post-arm.js.map +1 -0
- package/dist/agent/channels/notion-question-return.d.ts +77 -0
- package/dist/agent/channels/notion-question-return.d.ts.map +1 -0
- package/dist/agent/channels/notion-question-return.js +183 -0
- package/dist/agent/channels/notion-question-return.js.map +1 -0
- package/dist/agent/channels/notion-read.d.ts +62 -0
- package/dist/agent/channels/notion-read.d.ts.map +1 -0
- package/dist/agent/channels/notion-read.js +257 -0
- package/dist/agent/channels/notion-read.js.map +1 -0
- package/dist/agent/channels/notion-scope.d.ts +188 -0
- package/dist/agent/channels/notion-scope.d.ts.map +1 -0
- package/dist/agent/channels/notion-scope.js +433 -0
- package/dist/agent/channels/notion-scope.js.map +1 -0
- package/dist/agent/channels/notion-stop.d.ts +48 -0
- package/dist/agent/channels/notion-stop.d.ts.map +1 -0
- package/dist/agent/channels/notion-stop.js +141 -0
- package/dist/agent/channels/notion-stop.js.map +1 -0
- package/dist/agent/channels/notion-supervisor.d.ts +462 -0
- package/dist/agent/channels/notion-supervisor.d.ts.map +1 -0
- package/dist/agent/channels/notion-supervisor.js +482 -0
- package/dist/agent/channels/notion-supervisor.js.map +1 -0
- package/dist/agent/channels/notion-webhook-timing.d.ts +35 -0
- package/dist/agent/channels/notion-webhook-timing.d.ts.map +1 -0
- package/dist/agent/channels/notion-webhook-timing.js +99 -0
- package/dist/agent/channels/notion-webhook-timing.js.map +1 -0
- package/dist/agent/channels/notion-webhook-verifier.d.ts +5 -0
- package/dist/agent/channels/notion-webhook-verifier.d.ts.map +1 -0
- package/dist/agent/channels/notion-webhook-verifier.js +53 -0
- package/dist/agent/channels/notion-webhook-verifier.js.map +1 -0
- package/dist/agent/channels/notion-webhook.d.ts +242 -0
- package/dist/agent/channels/notion-webhook.d.ts.map +1 -0
- package/dist/agent/channels/notion-webhook.js +675 -0
- package/dist/agent/channels/notion-webhook.js.map +1 -0
- package/dist/agent/channels/notion-work-search.d.ts +9 -0
- package/dist/agent/channels/notion-work-search.d.ts.map +1 -0
- package/dist/agent/channels/notion-work-search.js +69 -0
- package/dist/agent/channels/notion-work-search.js.map +1 -0
- package/dist/agent/channels/notion-work.d.ts +68 -0
- package/dist/agent/channels/notion-work.d.ts.map +1 -0
- package/dist/agent/channels/notion-work.js +126 -0
- package/dist/agent/channels/notion-work.js.map +1 -0
- package/dist/agent/channels/notion.d.ts +95 -0
- package/dist/agent/channels/notion.d.ts.map +1 -0
- package/dist/agent/channels/notion.js +1081 -0
- package/dist/agent/channels/notion.js.map +1 -0
- package/dist/agent/contributions.d.ts +32 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +37 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/memory/blocks.d.ts +58 -0
- package/dist/agent/memory/blocks.d.ts.map +1 -0
- package/dist/agent/memory/blocks.js +264 -0
- package/dist/agent/memory/blocks.js.map +1 -0
- package/dist/agent/memory/memory.d.ts +157 -0
- package/dist/agent/memory/memory.d.ts.map +1 -0
- package/dist/agent/memory/memory.js +471 -0
- package/dist/agent/memory/memory.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +196 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +5 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +128 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/schedules/notion-ingress.d.ts +6 -0
- package/dist/agent/schedules/notion-ingress.d.ts.map +1 -0
- package/dist/agent/schedules/notion-ingress.js +16 -0
- package/dist/agent/schedules/notion-ingress.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +99 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/dist/agent/tools/notion_search.d.ts +56 -0
- package/dist/agent/tools/notion_search.d.ts.map +1 -0
- package/dist/agent/tools/notion_search.js +100 -0
- package/dist/agent/tools/notion_search.js.map +1 -0
- package/dist/agent/tools/read_notion_comments.d.ts +57 -0
- package/dist/agent/tools/read_notion_comments.d.ts.map +1 -0
- package/dist/agent/tools/read_notion_comments.js +236 -0
- package/dist/agent/tools/read_notion_comments.js.map +1 -0
- package/dist/agent/tools/read_notion_page.d.ts +53 -0
- package/dist/agent/tools/read_notion_page.d.ts.map +1 -0
- package/dist/agent/tools/read_notion_page.js +327 -0
- package/dist/agent/tools/read_notion_page.js.map +1 -0
- package/package.json +231 -0
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// notion-catch-up โ Notion's registered SEAT CATCH-UP FETCH (OPE-1726): the seat's own page
|
|
2
|
+
// discussion since its last wake, normalized for core's shared catch-up path
|
|
3
|
+
// (channels/seat-catch-up.ts).
|
|
4
|
+
//
|
|
5
|
+
// WHAT "THE SEAT'S CONTAINER" IS HERE. A Notion seat is bound to a PAGE and every wake arrives scoped
|
|
6
|
+
// to one DISCUSSION on it. The read is the PAGE's comment sweep, narrowed to that discussion here.
|
|
7
|
+
// ๐ด THERE IS NO PER-DISCUSSION ENDPOINT. Notion lists comments by their PAGE/BLOCK anchor, while a
|
|
8
|
+
// discussion id is only the reply/filter identity. For an INLINE trigger, ingress carries the
|
|
9
|
+
// fetched comment's authenticated parent block as `threadAnchor`; this fetch drills into that exact
|
|
10
|
+
// block, then filters by `threadRoot`. A PAGE-level trigger has no block anchor and preserves the
|
|
11
|
+
// full page sweep. Guessing the discussion id as a block id would 404 on every wake.
|
|
12
|
+
//
|
|
13
|
+
// WHY A STANDALONE CALLER, not `notionReadAdapter.readChannel`: that method takes a live
|
|
14
|
+
// {@link BindableContext} and runs the turn-time domain-scope fence over it. A catch-up fires from a
|
|
15
|
+
// webhook route with no turn, and its container is the one the wake itself already admitted. The
|
|
16
|
+
// CREDENTIAL is resolved the SAME way the read adapter resolves it โ from the DOMAIN that binds this
|
|
17
|
+
// surface (`notionChannelCredentialForSurface`), never the org default alone โ so a deployment whose
|
|
18
|
+
// Notion domain binds its own integration reads the page over that integration, and this opens no
|
|
19
|
+
// second transport: the same endpoint fence and the same 429 backoff.
|
|
20
|
+
//
|
|
21
|
+
// ๐ด THE VENDOR CANNOT FILTER BY TIME: Notion's comments endpoint takes no `since`, so the bound is
|
|
22
|
+
// applied on this side and again by the shared path.
|
|
23
|
+
//
|
|
24
|
+
// ๐ด โฆAND IT SERVES OLDEST-FIRST, WHICH IS WHAT RED `J-ENGAGED-notion` ON SHIP #208 (OPE-1831).
|
|
25
|
+
// THE CAUSE IS THE COMMENT CAP, CHARGED FROM THE OLD END. `readNotionComments` sweeps the PAGE's own
|
|
26
|
+
// comments first and only THEN walks the page's blocks. `maxComments` was charged over the whole
|
|
27
|
+
// history, so on the pinned journey page โ which had collected every prior ship's page-level comments
|
|
28
|
+
// โ the oldest hundred filled the cap and `take` returned STOP. The sweep ended with `blocksScanned`
|
|
29
|
+
// at ZERO: the block sweep never ran at all. The journey's own comment lives in a BLOCK-anchored
|
|
30
|
+
// discussion on a block the run appends itself (`scripts/e2e/scenarios.ts`: `appendParagraphBlock`
|
|
31
|
+
// then `createBlockDiscussion`), and a page-id comment query never returns a child block's comments
|
|
32
|
+
// (`scripts/e2e/notion-driver.ts` records that, live-confirmed). So the read returned `[]` with no
|
|
33
|
+
// error at all, on a discussion that HAD carried the message.
|
|
34
|
+
// THE FIX IS `sinceMs`, handed to the sweep so the cap counts comments IN the seat's window. The
|
|
35
|
+
// ancient page-level comments are then skipped without charging it, `take` does not stop, and the
|
|
36
|
+
// block sweep runs โ which is where the seat's discussion actually is.
|
|
37
|
+
// ๐ด `maxCommentPages` IS A SECOND, SEPARATE BOUND, and it is NOT what fixed ship #208. One call
|
|
38
|
+
// returns an anchor's oldest hundred comments and nothing follows `next_cursor`, so a PAGE-ANCHORED
|
|
39
|
+
// discussion on a busy page is unreachable for the same oldest-first reason. Real, and worth closing
|
|
40
|
+
// while the file is open โ but a page follow could never have reached a BLOCK-anchored comment, so do
|
|
41
|
+
// not read it as the ship #208 cause.
|
|
42
|
+
// ๐ด THE 80-BLOCK PAGE-SWEEP BOUND STILL EXISTS FOR PAGE-LEVEL CATCH-UP, AND IT IS NOW SPENT ON THE
|
|
43
|
+
// PAGE'S NEWEST BLOCKS (OPE-1843). It never hid the triggering INLINE discussion, because that path
|
|
44
|
+
// uses the authenticated block drill-in. A PAGE-level seat has no narrower anchor, so it keeps the
|
|
45
|
+
// bounded sweep โ but in document order the whole budget went to the oldest material of a page that
|
|
46
|
+
// had outgrown the cap, and Notion pages grow by APPENDING. A discussion on a recently appended
|
|
47
|
+
// block was therefore never scanned, and the read returned zero comments with no error: byte for
|
|
48
|
+
// byte the answer a quiet page gives. `blockScanOrder: "newest-first"` spends the same 80 scans on
|
|
49
|
+
// the last blocks instead, which is where a live conversation on an append-only page is.
|
|
50
|
+
// ๐ด IT MOVES THE BLIND SPOT RATHER THAN CLOSING IT: a comment posted today on an ANCIENT block of
|
|
51
|
+
// an over-cap page is now the unread case. Notion can neither query "blocks with recent comments"
|
|
52
|
+
// nor stamp a block when one is commented on, so no scan order is complete at any cap below the
|
|
53
|
+
// page's own length. `notion-api.ts` `blockScanOrder` carries the full reasoning.
|
|
54
|
+
//
|
|
55
|
+
// An unresolvable credential yields `[]` because no transport was attempted. An attempted read
|
|
56
|
+
// failure propagates to core's shared fail-soft boundary and records `outcome=failed` (OPE-1842).
|
|
57
|
+
// ๐ด THE ROW CANNOT CARRY THE RESIDUAL COVERAGE MISS, AND THAT IS A CONTRACT LIMIT, NOT A CHOICE.
|
|
58
|
+
// `readNotionComments` returns a `truncated` bit, but {@link SeatCatchUpFetch} answers with
|
|
59
|
+
// `NormalizedMessage[]` and nothing else, so a surface has no slot to report partial coverage in
|
|
60
|
+
// and `seat.catch_up` writes `outcome=empty raw=0 kept=0 reason=fetch-returned-none` for an
|
|
61
|
+
// over-cap miss exactly as for a quiet page. Closing that needs the shared fetch contract to carry
|
|
62
|
+
// a coverage bit โ core's decision, not this surface's.
|
|
63
|
+
import { readNotionComments, resolveNotionCredential } from "./notion-api.js";
|
|
64
|
+
import { notionChannelCredentialForSurface, notionIntegrations, NOTION_CHANNEL_CREDENTIAL } from "./notion-credentials.js";
|
|
65
|
+
import { resolveNotionBotUser } from "./notion-bot-user.js";
|
|
66
|
+
import { catchUpSender, getConfig } from "@open-everywhere/core/agent/adapter-api.js";
|
|
67
|
+
/** The loaded domain roster, or none. FAIL-SOFT: a config that will not load must not throw out of a
|
|
68
|
+
* read contracted never to throw โ it degrades to the org-default credential, which is what a
|
|
69
|
+
* surface bound to no domain resolves anyway. */
|
|
70
|
+
function safeDomains() {
|
|
71
|
+
try {
|
|
72
|
+
return getConfig().loadDomains();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** One sweep. The catch-up window is "since the seat last woke", and a page that collected more than
|
|
79
|
+
* this in that window is one core's token cap would trim regardless. */
|
|
80
|
+
const CATCH_UP_MAX_COMMENTS = 100;
|
|
81
|
+
/** Blocks the page sweep scans. Matches the read adapter's own bound, so a catch-up never costs more
|
|
82
|
+
* API calls than a `read_channel` on the same page. */
|
|
83
|
+
const CATCH_UP_MAX_BLOCK_SCANS = 80;
|
|
84
|
+
/**
|
|
85
|
+
* Pages of comments the sweep follows PER ANCHOR (`notion-api.ts listBlockComments`).
|
|
86
|
+
* ๐ด ONE PAGE IS THE OLDEST HUNDRED, WHICH IS WHY THIS IS NOT 1 (OPE-1831). Notion serves comments
|
|
87
|
+
* oldest-first, so an anchor that has collected more than a page puts today's comment on the LAST
|
|
88
|
+
* page and the default single call can never reach it. A PAGE-ANCHORED discussion on any long-lived
|
|
89
|
+
* doc then reads as permanently quiet to its seat, with no error to show for it.
|
|
90
|
+
* IT COSTS NOTHING ON A NORMAL ANCHOR: the follow stops the moment Notion reports no more, so an
|
|
91
|
+
* anchor under a hundred comments is still exactly one call. Only an overflowing anchor pays, and
|
|
92
|
+
* five pages is five hundred comments โ far past any window a seat's last wake can span.
|
|
93
|
+
*
|
|
94
|
+
* ๐ด THE PER-WAKE CALL BUDGET, STATED BECAUSE THIS CHANGE GREW IT (OPE-1831). One catch-up now costs
|
|
95
|
+
* up to `1 + CATCH_UP_MAX_BLOCK_SCANS` list calls (the page anchor, then one per block), each up to
|
|
96
|
+
* `CATCH_UP_MAX_COMMENT_PAGES` pages โ so 81 calls typically and 405 at the theoretical ceiling,
|
|
97
|
+
* against the shared 60 s-per-call 429 budget (`channels/rate-limit-retry.ts`, OPE-1254).
|
|
98
|
+
* ๐ด AND THE OLD CHEAP PATH WAS THE BUG. Before `sinceMs`, a page whose own comments filled
|
|
99
|
+
* `maxComments` returned after ONE call โ cheap, and WRONG: that early return is exactly what stopped
|
|
100
|
+
* the block sweep and shipped #208. Paying the sweep is the correction, not a regression to undo.
|
|
101
|
+
* The 405 ceiling needs every one of 81 anchors to hold over 100 comments, which no real page does;
|
|
102
|
+
* the realistic cost is one extra call or two on the page anchor alone.
|
|
103
|
+
*/
|
|
104
|
+
const CATCH_UP_MAX_COMMENT_PAGES = 5;
|
|
105
|
+
/**
|
|
106
|
+
* Normalize ONE Notion comment, applying the VISIBILITY GATE every fetch adapter owes core: a comment
|
|
107
|
+
* with no author, no text or no readable time is dropped, because an unattributable body must never
|
|
108
|
+
* reach a seat. The deployment's OWN integration id resolves `self`.
|
|
109
|
+
*
|
|
110
|
+
* ๐ด `authorKind` IS ABSENT ON ALMOST EVERY COMMENT, AND READING THAT AS "NOT A PERSON" IS OPE-1857.
|
|
111
|
+
* It is Notion's own actor type, and Notion serves a comment's `created_by` as a PARTIAL user โ
|
|
112
|
+
* `id` and `object`, no `type` (notion-api.ts, whose interface says so). This line used to read
|
|
113
|
+
* `authorKind === "person" ? "human" : "bot"`, so EVERY comment this surface read arrived tagged
|
|
114
|
+
* `bot` โ and the default `wide` admission policy refuses every bot, on every wake, for ever. Ship
|
|
115
|
+
* #212 measured it: `seat.catch_up outcome=empty raw=4 kept=0 reason=admission-dropped-all` on a
|
|
116
|
+
* read the bounds had kept in full. A real page member's comment was refused by that arithmetic
|
|
117
|
+
* exactly as the e2e driver's was, so the seat read a page it was being spoken on as silent.
|
|
118
|
+
* ๐ด THE RULE IS CORE'S NOW ({@link catchUpSender}), and this surface contributes only the SIGNAL:
|
|
119
|
+
* a kind Notion actually stated, or `undefined` when it stated none. The repository's own trigger
|
|
120
|
+
* road already read it this way โ `classifyNotionComment` (notion-supervisor.ts) drops on
|
|
121
|
+
* `author?.type && author.type !== "person"`, so an absent type is a person there too. The two roads
|
|
122
|
+
* disagreeing about one comment is the whole defect.
|
|
123
|
+
*/
|
|
124
|
+
function normalizeComment(c, selfActorIds) {
|
|
125
|
+
const text = c.text.trim();
|
|
126
|
+
if (text.length === 0 || !c.authorId || !c.discussionId)
|
|
127
|
+
return undefined;
|
|
128
|
+
const ms = Date.parse(c.createdTime ?? "");
|
|
129
|
+
if (!Number.isFinite(ms))
|
|
130
|
+
return undefined;
|
|
131
|
+
// OUR OWN VOICE IS THE DEPLOYMENT'S, NOT ONE INTEGRATION'S (OPE-1921) โ see collectCatchUpSelfIds.
|
|
132
|
+
const isSelf = selfActorIds.has(c.authorId);
|
|
133
|
+
return {
|
|
134
|
+
// ๐ด THE COMMENT'S OWN NOTION ID, AND IT HAS TO BE (OPE-1847). The wake route passes the trigger's
|
|
135
|
+
// comment id as `excludeId`, and core removes the wake's own message BY ID alone โ time cannot do
|
|
136
|
+
// it here, because `created_time` floors to the MINUTE and the trigger therefore shares its
|
|
137
|
+
// instant with every comment the seat is reading for. This slot used to hold a render label
|
|
138
|
+
// (`<discussion>:<created_time>`) that could never equal that id, so the trigger had to be bounded
|
|
139
|
+
// out by `untilMs` โ which took the seat's whole catch-up with it and shipped #211 red.
|
|
140
|
+
// The label survives only as the fallback for a comment Notion served with no id, where nothing
|
|
141
|
+
// better exists; it is NOT unique (a coarse stamp lets two comments in one discussion collide).
|
|
142
|
+
id: c.id ?? `${c.discussionId}:${c.createdTime ?? ms}`,
|
|
143
|
+
ms,
|
|
144
|
+
// The bot's OWN posts carry no author id, matching every other surface's normalized shape.
|
|
145
|
+
...(isSelf ? {} : { authorId: c.authorId }),
|
|
146
|
+
sender: catchUpSender({ self: isSelf, ...(c.authorKind === undefined ? {} : { bot: c.authorKind !== "person" }) }),
|
|
147
|
+
// A Notion wake is thread-first: every comment belongs to a discussion, which IS the thread.
|
|
148
|
+
type: "thread",
|
|
149
|
+
threadId: c.discussionId,
|
|
150
|
+
text,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const liveDeps = {
|
|
154
|
+
readComments: readNotionComments,
|
|
155
|
+
resolveCredential: resolveNotionCredential,
|
|
156
|
+
selfActorIdFor: async (credentialId, credential) => (await resolveNotionBotUser(credentialId, credential)()).id,
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* EVERY configured integration's bot user id, for the same reason the ingress road takes a SET
|
|
160
|
+
* (OPE-1921): this deployment posts under one bot user id PER INTEGRATION, and a reply a SIBLING
|
|
161
|
+
* integration posted lands in the very discussion this read sweeps. Against the reading
|
|
162
|
+
* integration's id alone it matched nothing, so the seat was handed its own words as another
|
|
163
|
+
* person's โ at the `unauth` floor, in the context it then answers from.
|
|
164
|
+
*
|
|
165
|
+
* ๐ด THE BLOCKER THIS FILE USED TO NAME IS GONE. It said closing this needed the DELIVERY's
|
|
166
|
+
* credential id on the shared `SeatCatchUpRequest`, which carries none. That was true only while the
|
|
167
|
+
* answer had to be ONE id: the set does not care which integration posted, so no new request field
|
|
168
|
+
* is needed. What remains is the ordinary residual โ an integration whose id will not resolve.
|
|
169
|
+
*
|
|
170
|
+
* FAIL-SOFT PER INTEGRATION, and DELIBERATELY NOT fail-closed like the ingress road. An id this
|
|
171
|
+
* cannot resolve just leaves that integration's comments reading as a person's, which is where this
|
|
172
|
+
* read already stood; blinding the catch-up on a transient `GET /v1/users/me` failure would
|
|
173
|
+
* re-create the OPE-1857 symptom โ a seat that keeps nothing.
|
|
174
|
+
* ๐ด AND THE INGRESS ARM DOES NOT COVER THIS ONE, SO DO NOT READ IT AS A BACKSTOP. An unresolved
|
|
175
|
+
* PRIMARY id blocks every wake, so that case really does take no catch-up. An unresolved SIBLING
|
|
176
|
+
* does not: `classifyNotionComment` step 5 drops only authors Notion does not mark `person`, so a
|
|
177
|
+
* person-marked comment still wakes a turn, still takes a catch-up, and that integration's own
|
|
178
|
+
* prior replies then enter the seat's context as a person's words at the `unauth` floor. That
|
|
179
|
+
* residual is the price of the fail-soft posture here, and it is stated rather than covered.
|
|
180
|
+
*
|
|
181
|
+
* Cost: one `GET /v1/users/me` per SIBLING integration, memoised for the process lifetime by
|
|
182
|
+
* `resolveNotionBotUser`'s per-credential cache โ against the up-to-81 list calls the sweep itself
|
|
183
|
+
* makes. The sibling's CREDENTIAL is passed as a thunk, so on a cache hit it is never resolved at
|
|
184
|
+
* all; `resolveNotionCredential` has no memo of its own, and an eager resolve here would charge
|
|
185
|
+
* every wake a store read and a decrypt. A single-integration deployment resolves exactly what it
|
|
186
|
+
* did before.
|
|
187
|
+
*/
|
|
188
|
+
async function collectCatchUpSelfIds(readingCredentialId, readingCredential, domains, deps) {
|
|
189
|
+
const ids = new Set();
|
|
190
|
+
const add = async (credentialId, credential) => {
|
|
191
|
+
try {
|
|
192
|
+
const id = await deps.selfActorIdFor?.(credentialId, credential);
|
|
193
|
+
if (id)
|
|
194
|
+
ids.add(id);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* an id we cannot resolve leaves that integration's comments reading as a person's */
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
await add(readingCredentialId, async () => readingCredential);
|
|
201
|
+
for (const integration of notionIntegrations(domains)) {
|
|
202
|
+
if (integration.credentialId === readingCredentialId)
|
|
203
|
+
continue;
|
|
204
|
+
// ๐ด A THUNK, NOT A RESOLVED CREDENTIAL. `resolveNotionBotUser` memoises the bot user per
|
|
205
|
+
// credential and NEVER CALLS the thunk on a cache hit, while `resolveNotionCredential` has no
|
|
206
|
+
// memo of its own โ so resolving eagerly here would charge every wake an operational-store read
|
|
207
|
+
// and a decrypt for an answer the cache already holds, and would write one
|
|
208
|
+
// credential-unresolved audit row per wake, for ever, on a sibling with no stored secret.
|
|
209
|
+
await add(integration.credentialId, () => deps.resolveCredential(integration.credentialId));
|
|
210
|
+
}
|
|
211
|
+
return ids;
|
|
212
|
+
}
|
|
213
|
+
/** The fetch, with its seams exposed so the contract test drives it with no network. */
|
|
214
|
+
export async function fetchNotionCatchUpWindow(req, deps = liveDeps) {
|
|
215
|
+
// The DOMAIN's own integration where this surface binds one, else the org default โ the same
|
|
216
|
+
// resolution `notionReadAdapter` makes, so the catch-up reads the page over the credential the
|
|
217
|
+
// operator selected for it rather than a broader one.
|
|
218
|
+
const domains = deps.domains ?? safeDomains();
|
|
219
|
+
const credentialId = req.channelKey ? notionChannelCredentialForSurface(req.channelKey, domains) : undefined;
|
|
220
|
+
const cred = await deps.resolveCredential(credentialId);
|
|
221
|
+
if (!cred)
|
|
222
|
+
return []; // unconfigured โ nothing to read, never a guessed key
|
|
223
|
+
// OUR OWN IDENTITY, under the SAME key the ingress road caches it by: an absent `credentialId`
|
|
224
|
+
// means the ORG DEFAULT, which is stored as `NOTION_CHANNEL_CREDENTIAL` โ so both roads memoise
|
|
225
|
+
// one integration under one key rather than resolving it twice. EVERY configured integration's id,
|
|
226
|
+
// for the reason the ingress road takes the set (OPE-1921) โ see `selfActorIds` below.
|
|
227
|
+
const readingCredentialId = credentialId ?? NOTION_CHANNEL_CREDENTIAL;
|
|
228
|
+
const selfActorIds = deps.selfActorId !== undefined
|
|
229
|
+
? new Set([deps.selfActorId])
|
|
230
|
+
: await collectCatchUpSelfIds(readingCredentialId, cred, domains, deps);
|
|
231
|
+
const res = await deps.readComments(req.container, {
|
|
232
|
+
// ๐ด THE ENDPOINT FENCE TRAVELS WITH THE KEY, unconditionally and ON ONE LINE: a bare `apiKey`
|
|
233
|
+
// silently disables it, and `notion-credential-fence-test.ts` ยง2 reads the pair line by line โ
|
|
234
|
+
// so splitting them reads as a bare key to the rule that exists to stop one. An absent
|
|
235
|
+
// `endpoints` is already the unnarrowed reading, so the pair is always passed together.
|
|
236
|
+
apiKey: cred.apiKey, endpoints: cred.endpoints,
|
|
237
|
+
maxComments: CATCH_UP_MAX_COMMENTS,
|
|
238
|
+
maxBlockScans: CATCH_UP_MAX_BLOCK_SCANS,
|
|
239
|
+
// ๐ด THE WINDOW GOES TO THE SWEEP, not only to the loop below (OPE-1831). `maxComments` is
|
|
240
|
+
// charged in the order Notion serves โ OLDEST FIRST โ so a cap applied over the whole history
|
|
241
|
+
// fills with a page's most ancient comments and truncates before it reaches the seat's own
|
|
242
|
+
// window. Handing the bound down makes the cap count comments IN the window instead.
|
|
243
|
+
sinceMs: req.sinceMs,
|
|
244
|
+
maxCommentPages: CATCH_UP_MAX_COMMENT_PAGES,
|
|
245
|
+
// ๐ด THE BLOCK BUDGET IS SPENT ON THE PAGE'S NEWEST BLOCKS (OPE-1843). Document order spends
|
|
246
|
+
// all 80 scans on the oldest material of a page that has outgrown the cap, so a discussion on a
|
|
247
|
+
// recently appended block is never scanned and the seat reads a quiet room. A catch-up asks
|
|
248
|
+
// "what happened since I last woke", and the growth shape that crosses the cap is append-only.
|
|
249
|
+
// No effect on a page under the cap, and none on the `block` drill-in above, which scans one
|
|
250
|
+
// named block. The residual โ a new comment on an ANCIENT block of an over-cap page โ is
|
|
251
|
+
// `readNotionComments`'s own to state, and it does: see `blockScanOrder`.
|
|
252
|
+
blockScanOrder: "newest-first",
|
|
253
|
+
// An inline comment's authenticated parent block addresses the exact discussion anchor. The
|
|
254
|
+
// discussion id is not a block id and remains only the filter below.
|
|
255
|
+
...(req.threadAnchor ? { block: req.threadAnchor } : {}),
|
|
256
|
+
});
|
|
257
|
+
const out = [];
|
|
258
|
+
for (const c of res.comments) {
|
|
259
|
+
// The seat's own discussion, filtered HERE โ see the header for why there is no endpoint that
|
|
260
|
+
// could do it. A seat bound to no discussion reads the whole page.
|
|
261
|
+
if (req.threadRoot !== undefined && c.discussionId !== req.threadRoot)
|
|
262
|
+
continue;
|
|
263
|
+
const n = normalizeComment(c, selfActorIds);
|
|
264
|
+
if (n && n.ms >= req.sinceMs)
|
|
265
|
+
out.push(n);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* ๐ด NOTION FLOORS `created_time` TO THE MINUTE, and this is the number the shared catch-up road
|
|
271
|
+
* widens both of its bounds by (OPE-1847, `core/agent/channels/seat-catch-up.ts`
|
|
272
|
+
* {@link SeatCatchUpDeclaration.timestampPrecisionMs}).
|
|
273
|
+
* It is the VENDOR'S UNIT, not a margin: every comment posted inside one wall-clock minute comes back
|
|
274
|
+
* carrying that minute's opening instant. Declare it too small and a comment in the trigger's own
|
|
275
|
+
* minute is bounded out of the window it belongs to โ the ship #211 red, where the seat read two
|
|
276
|
+
* comments and kept none. Declare it too large and the seat is handed comments from before its window.
|
|
277
|
+
* Measured on the live workspace (OPE-1711) and the repository's own e2e driver floors to the same
|
|
278
|
+
* unit (`core/scripts/e2e/notion-driver.ts` `notionMinuteFloor`).
|
|
279
|
+
*/
|
|
280
|
+
export const NOTION_TIMESTAMP_PRECISION_MS = 60_000;
|
|
281
|
+
/** Notion's registered {@link SeatCatchUpFetch} โ the one thing this surface contributes to the shared path. */
|
|
282
|
+
export const notionSeatCatchUpFetch = (req) => fetchNotionCatchUpWindow({
|
|
283
|
+
container: req.container,
|
|
284
|
+
...(req.threadRoot ? { threadRoot: req.threadRoot } : {}),
|
|
285
|
+
...(req.threadAnchor ? { threadAnchor: req.threadAnchor } : {}),
|
|
286
|
+
sinceMs: req.sinceMs,
|
|
287
|
+
...(req.channelKey ? { channelKey: req.channelKey } : {}),
|
|
288
|
+
});
|
|
289
|
+
//# sourceMappingURL=notion-catch-up.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notion-catch-up.js","sourceRoot":"","sources":["../../../agent/channels/notion-catch-up.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { type BindableContext, type Domain, type ConnectorIntegration } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
/** The provider name in `agent/access/providers.ts`. */
|
|
3
|
+
export declare const NOTION_PROVIDER = "notion";
|
|
4
|
+
/** The ORG-DEFAULT channel-plumbing credential id โ the role a domain override replaces. */
|
|
5
|
+
export declare const NOTION_CHANNEL_CREDENTIAL = "channel-key";
|
|
6
|
+
/**
|
|
7
|
+
* The notion `channel-key` credential id a page's domain binds, or **undefined when it binds none**.
|
|
8
|
+
*
|
|
9
|
+
* Undefined is a load-bearing answer, not a failure: it means "this page's domain asked for no
|
|
10
|
+
* override", which every caller reads as "use the org default". Keeping it distinct from a resolved
|
|
11
|
+
* id is what lets the webhook route fall through to its own arms (the verifying integration, then
|
|
12
|
+
* the org default) instead of treating "no binding" as a refusal.
|
|
13
|
+
*
|
|
14
|
+
* Pure โ `domains` is injected, exactly like `notionPageDomain`'s, so the whole decision is
|
|
15
|
+
* unit-testable with no config and no store.
|
|
16
|
+
*/
|
|
17
|
+
export declare function notionChannelCredentialFor(workspace: string | undefined, pageId: string | undefined, domains: readonly Domain[]): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* {@link notionChannelCredentialFor} for a session's own surface key (`notion/<workspace>/<page>`) โ
|
|
20
|
+
* the form `resolvePartition(ctx).surfaceKey` produces.
|
|
21
|
+
*
|
|
22
|
+
* The read tools resolve the CALLER'S surface, not the page argument. That is deliberate and it is
|
|
23
|
+
* not a weaker check: `notion-scope.ts` has already established that an admitted read is either the
|
|
24
|
+
* caller's own page or a page in the caller's own domain, so the two resolve to the same domain by
|
|
25
|
+
* construction โ and the caller's surface is known BEFORE the fence runs, which the block-resolution
|
|
26
|
+
* walk needs (it must make a Notion call to find a block's owning page).
|
|
27
|
+
*/
|
|
28
|
+
export declare function notionChannelCredentialForSurface(surfaceKey: string, domains: readonly Domain[]): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The credential a READ TOOL runs under (OPE-499). **The delivery's own carried credential wins**
|
|
31
|
+
* when this session was woken by one โ the same `credential_id` auth attribute the reply arm already
|
|
32
|
+
* reads (notion-post-arm.ts) โ so a session only ever reads what the integration that verified its
|
|
33
|
+
* delivery was actually shared into, never a wider domain binding a double-shared page could reach.
|
|
34
|
+
*
|
|
35
|
+
* Falls back to the CALLING SESSION's own domain binding when no delivery credential rides the
|
|
36
|
+
* context โ an agent-initiated session (a routine, a proactive post) never carried one, and that is
|
|
37
|
+
* the pre-OPE-499 rule, unchanged. Fail-soft throughout: a synthetic or identity-less ctx has no
|
|
38
|
+
* partition to read, so it resolves undefined and the caller falls back to the org default, exactly
|
|
39
|
+
* like an unbound domain. That is the correct direction here: the DOMAIN FENCE (notion-scope.ts) is
|
|
40
|
+
* what refuses an out-of-scope read, and it runs whether or not a credential was selected.
|
|
41
|
+
*/
|
|
42
|
+
export declare function notionCallerCredential(ctx: BindableContext, domains: readonly Domain[]): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Every Notion integration this deployment is reachable by โ the ORG DEFAULT FIRST, then one per
|
|
45
|
+
* domain that binds its own. The ingress tries each entry's inbound secret in this order.
|
|
46
|
+
*/
|
|
47
|
+
export declare function notionIntegrations(domains: readonly Domain[]): readonly ConnectorIntegration[];
|
|
48
|
+
/**
|
|
49
|
+
* Does the integration that SIGNED a delivery serve `owner`? The one comparison behind both signer
|
|
50
|
+
* fences โ the trigger plane's {@link notionTriggerScopeVerdict} and the comment plane's
|
|
51
|
+
* {@link notionDeliveryScopeVerdict} โ extracted so the two can never answer it differently.
|
|
52
|
+
*
|
|
53
|
+
* Three outcomes, because the caller's log line needs to tell them apart:
|
|
54
|
+
* โข `"credential"` โ the signer IS the credential this domain binds. The ordinary yes.
|
|
55
|
+
* โข `"shared-secret"` โ a different credential, signed with the SAME inbound secret. Accepted, and
|
|
56
|
+
* named, because `connectorIntegrations` defaults a domain's `inboundSecret` to the deployment
|
|
57
|
+
* default: a domain that binds a credential without declaring its own secret signs exactly like
|
|
58
|
+
* the org default, and refusing would break a supported config in which the domain runs no
|
|
59
|
+
* subscription of its own.
|
|
60
|
+
* โข `served: false` โ neither, so the signer cannot speak for this domain.
|
|
61
|
+
*
|
|
62
|
+
* ๐ด THE PROPERTY THIS BUYS, STATED HONESTLY: **a domain is fenced from another domain's forged
|
|
63
|
+
* deliveries IFF it runs its own inbound secret.** That is not a hole either fence opens. It is the
|
|
64
|
+
* limit of what an HMAC can tell one sender from another when two senders hold the same key.
|
|
65
|
+
*
|
|
66
|
+
* `expected` rides on every outcome so a refusal can name the credential that WOULD have served.
|
|
67
|
+
*/
|
|
68
|
+
export type NotionSignerVerdict = {
|
|
69
|
+
readonly served: true;
|
|
70
|
+
readonly via: "credential" | "shared-secret";
|
|
71
|
+
readonly expected: string;
|
|
72
|
+
} | {
|
|
73
|
+
readonly served: false;
|
|
74
|
+
readonly expected: string;
|
|
75
|
+
};
|
|
76
|
+
export declare function notionSignerServesDomain(owner: Domain, signerCredentialId: string, integrations: readonly ConnectorIntegration[]): NotionSignerVerdict;
|
|
77
|
+
/**
|
|
78
|
+
* Every domain a COMMENT delivery reaches THROUGH ITS WORKSPACE CLAIM โ the domains the payload's
|
|
79
|
+
* `workspace_id` can steer, and therefore the only ones {@link notionDeliveryScopeVerdict} fences.
|
|
80
|
+
*
|
|
81
|
+
* ๐ด TWO RESOLVERS ANSWER "WHICH DOMAIN?" DOWNSTREAM, AND THEY DO NOT ALWAYS AGREE. Both are
|
|
82
|
+
* consulted on the session the delivery opens, so a check on one alone would leave the other open:
|
|
83
|
+
*
|
|
84
|
+
* โข {@link notionPageDomain} โ the credential binding (agent-initiated work) and the read fence
|
|
85
|
+
* (notion-scope.ts). Its roster leg compares the PAGE-ID segment alone.
|
|
86
|
+
* โข `resolveDomainForChannel` โ the ambient gate, the engagement tier, the supervisor grain and
|
|
87
|
+
* the external-author policy, all keyed on the `notion/<workspace>/<page>` surface key. Its
|
|
88
|
+
* roster leg compares the WHOLE key, so a workspace that does not match the `reach` entry
|
|
89
|
+
* falls THROUGH to the claim leg.
|
|
90
|
+
*
|
|
91
|
+
* ๐ด WHY THE ROSTER LEGS ARE EXEMPT, AND THIS IS THE WHOLE DESIGN. A roster hit is not steerable by
|
|
92
|
+
* the payload's workspace:
|
|
93
|
+
* โข {@link notionPageDomain}'s roster leg ignores the workspace outright, and OPE-496 established
|
|
94
|
+
* the page id from the comment Notion returned under the verified credential. Authenticated fact
|
|
95
|
+
* in, config match out.
|
|
96
|
+
* โข `resolveDomainForChannel`'s roster leg needs the WHOLE key to appear in `reach`, so the
|
|
97
|
+
* workspace it accepts is one the deployer typed themselves.
|
|
98
|
+
* A claim leg is the opposite: a container entry names a workspace, the payload names a workspace, and a
|
|
99
|
+
* match hands over every page under it.
|
|
100
|
+
*
|
|
101
|
+
* KEEPING THE ROSTER LEGS OUT IS LOAD-BEARING, not a shortcut. A page shared with two integrations
|
|
102
|
+
* produces a delivery on EACH subscription, and each one must run under its own verifying integration
|
|
103
|
+
* (the header's "prefer, don't refuse" rule, and the escalation OPE-499 closes by carrying the
|
|
104
|
+
* delivery's credential into the reads). Fencing a rostered page on the signer would refuse the
|
|
105
|
+
* second subscription's delivery outright and delete that behaviour.
|
|
106
|
+
*
|
|
107
|
+
* Pure โ `domains` is injected, exactly like {@link notionPageDomain}'s.
|
|
108
|
+
*/
|
|
109
|
+
export declare function notionDeliveryClaimedDomains(workspaceId: string | undefined, pageId: string, domains: readonly Domain[]): readonly Domain[];
|
|
110
|
+
/** {@link notionDeliveryScopeVerdict}'s answer. `ok` is the ONLY field a caller may branch on; the
|
|
111
|
+
* `reason` and the `detail` are operator-facing prose for the log line and carry no control flow. */
|
|
112
|
+
export type NotionDeliveryScopeVerdict = {
|
|
113
|
+
readonly ok: true;
|
|
114
|
+
readonly detail: string;
|
|
115
|
+
} | {
|
|
116
|
+
readonly ok: false;
|
|
117
|
+
readonly reason: string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
121
|
+
* ๐ด THE COMMENT-PLANE INGRESS FENCE (OPE-502) โ MAY THIS SIGNER OPEN A SESSION ON THIS PAGE?
|
|
122
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
123
|
+
*
|
|
124
|
+
* `notion-webhook.ts` calls this after the page is established (OPE-496) and BEFORE the surface key
|
|
125
|
+
* decides anything. The rule is the trigger plane's rule, on the plane next door:
|
|
126
|
+
*
|
|
127
|
+
* A delivery may open a session on a domain it reaches THROUGH ITS WORKSPACE CLAIM only when that
|
|
128
|
+
* domain is one the SIGNING integration serves.
|
|
129
|
+
*
|
|
130
|
+
* WHY THE WORKSPACE NEEDS THIS AND THE PAGE DOES NOT. OPE-496 established the PAGE from the comment
|
|
131
|
+
* Notion returned under the verified credential, so the page is an authenticated fact. It left the
|
|
132
|
+
* WORKSPACE where it found it โ `ev.workspace_id`, a payload field nothing authenticates โ and the
|
|
133
|
+
* two are composed into `notion/<workspace>/<page>`. There is no verified source for the second
|
|
134
|
+
* half: Notion's REST surface returns no workspace id on any object (`/v1/users/me` gives
|
|
135
|
+
* `bot.workspace_name`, not an id), so no fetch under any credential can confirm the claim. Pinning
|
|
136
|
+
* the workspace to a config constant is wrong for a different reason โ one deployment may hold
|
|
137
|
+
* several workspaces. So the claim is fenced instead of proved, exactly as OPE-500 fenced the
|
|
138
|
+
* trigger plane's scope path.
|
|
139
|
+
*
|
|
140
|
+
* WORKED THROUGH, because the point is what it REFUSES. Domain `security` claims workspace
|
|
141
|
+
* `notion/W-sec` in a container entry and binds integration `notion-security` with its own inbound
|
|
142
|
+
* secret. A holder of domain `payments`'s `verification_token` comments on a page their OWN
|
|
143
|
+
* integration can see, then signs a delivery whose `workspace_id` reads `W-sec`. The page reaches
|
|
144
|
+
* `security` through that claim, so without this fence the session opens on `security` โ with
|
|
145
|
+
* `security`'s ambient flag, `security`'s read scope over every page under the claim, and (for an
|
|
146
|
+
* agent-initiated call) `security`'s credential. The fence reads that domain's credential, finds
|
|
147
|
+
* `notion-security` against a `notion-payments` signature, and refuses.
|
|
148
|
+
*
|
|
149
|
+
* FAIL CLOSED, AND THE REFUSALS ARE REAL:
|
|
150
|
+
* โข NO SIGNER โ refuse. `webhooks.allowUnverifiedNotion` accepts a delivery with no secret
|
|
151
|
+
* configured anywhere, so nothing identifies a sender and the fence has no fact to read. A fence
|
|
152
|
+
* that says yes when it cannot tell is not a fence. The consequence is stated in the runbook: with
|
|
153
|
+
* that dev hatch on, a CLAIMED page opens no session. A rostered page is unaffected, so the local
|
|
154
|
+
* exercise the hatch exists for still runs.
|
|
155
|
+
* โข NO CLAIMED DOMAIN โ allow. Either the page is rostered โ see
|
|
156
|
+
* {@link notionDeliveryClaimedDomains} for why a roster hit is not steerable โ or it is in no
|
|
157
|
+
* domain at all, where the `org` pseudo-domain answers and no payload field can steer that
|
|
158
|
+
* either.
|
|
159
|
+
*
|
|
160
|
+
* THE PARTITION IS DECIDED SEPARATELY (OPE-254 ยง2, folding OPE-537). This fence decides the domain
|
|
161
|
+
* BOUNDARY; which partition a delivery lands in is notion-webhook.ts's step 8c, which composes the
|
|
162
|
+
* key from the workspace segment of the `reach` entry that MATCHED โ `notionReachWorkspace`, on both
|
|
163
|
+
* legs. A page named outright therefore keys on config's workspace whatever a delivery claims. A
|
|
164
|
+
* page reached by a CONTAINER only matches when the payload's workspace equals the container's own
|
|
165
|
+
* text, so there the derived value equals the payload's by construction; a delivery naming a
|
|
166
|
+
* workspace no domain reaches matches nothing, and its key belongs to no domain โ no memory, no
|
|
167
|
+
* bundle, and the ambient gate refuses it โ rather than opening a second partition inside one.
|
|
168
|
+
*
|
|
169
|
+
* ๐ด WHY THE CONTAINER LEG NEEDED A CONFIG RULE, NOT JUST THE SAME READ. The exact-key match is
|
|
170
|
+
* workspace-BLIND โ it finds a page by id alone, so the workspace read back off the matched entry
|
|
171
|
+
* is a signal INDEPENDENT of whatever the payload said. The container leg is the opposite shape:
|
|
172
|
+
* `notionPageDomain`'s container match composes `notion/<payload-workspace>/<page>` and only admits
|
|
173
|
+
* it when a container entry's tenant segment equals THAT SAME payload workspace verbatim
|
|
174
|
+
* (surface-key-grammar.ts refuses a glob on the tenant segment, so the compare is always exact). So
|
|
175
|
+
* whenever a container admits a delivery, the matched entry's workspace text and the payload's are,
|
|
176
|
+
* by construction, the identical string โ reading the entry back is a no-op IN VALUE.
|
|
177
|
+
*
|
|
178
|
+
* What it is not is a no-op in PROPERTY, and that is the point. The remaining way one page could
|
|
179
|
+
* still key two ways was a domain reaching MORE THAN ONE Notion workspace: two honestly-signed
|
|
180
|
+
* deliveries for the same page, one naming each, opened two partitions. memory/domain.ts's
|
|
181
|
+
* `partitionTenantProblems` now refuses that config at load, so the matched entry names exactly one
|
|
182
|
+
* workspace per domain and the partition is a fact about config rather than about a delivery.
|
|
183
|
+
* No live re-keying follows: on every configuration that still loads, the derived value equals the
|
|
184
|
+
* value the payload carried, so no existing `channel_session` or memory row changes key.
|
|
185
|
+
*
|
|
186
|
+
* Pure โ `domains` is injected, so the whole decision is unit-testable with no config and no store.
|
|
187
|
+
*/
|
|
188
|
+
export declare function notionDeliveryScopeVerdict(workspaceId: string | undefined, pageId: string, signerCredentialId: string | undefined, domains: readonly Domain[]): NotionDeliveryScopeVerdict;
|
|
189
|
+
/**
|
|
190
|
+
* One integration's INBOUND signing key by operator-secret name, or undefined โ and the ONE place
|
|
191
|
+
* that decides whether a failure is REPORTED.
|
|
192
|
+
*
|
|
193
|
+
* ๐ด THE FAILURE THAT MUST BE LOUD. "Nobody stored this name" is the ordinary state for an
|
|
194
|
+
* integration the operator never configured: it contributes no key, a delivery it would have signed
|
|
195
|
+
* simply fails the gate, and saying so on every POST would be noise. Every OTHER failure โ an
|
|
196
|
+
* unarmed `OE_CREDENTIAL_ENCRYPTION_KEY`, a store that will not open โ makes EVERY delivery on that
|
|
197
|
+
* integration 401 with nothing in the log to explain it. It is reported.
|
|
198
|
+
*
|
|
199
|
+
* The two are told apart by the ERROR CLASS ({@link OperatorSecretNotSetError}), never by the
|
|
200
|
+
* message: the unarmed-key message ALSO contains the words "is not set", so the substring test this
|
|
201
|
+
* replaces reported neither and a broken credential store went silent.
|
|
202
|
+
*
|
|
203
|
+
* `get` and `log` are injected so the decision is testable with no store and no eve boot; the route
|
|
204
|
+
* passes `getOperatorSecret` and its own channel logger. The report names the SECRET NAME and the
|
|
205
|
+
* reason, never a value. Both arms return undefined โ this function never throws into a webhook.
|
|
206
|
+
*/
|
|
207
|
+
export declare function resolveNotionInboundSecret(name: string, get: (secretName: string) => Promise<{
|
|
208
|
+
readonly value: string;
|
|
209
|
+
}>, log: (message: string) => void): Promise<string | undefined>;
|
|
210
|
+
//# sourceMappingURL=notion-credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notion-credentials.d.ts","sourceRoot":"","sources":["../../../agent/channels/notion-credentials.ts"],"names":[],"mappings":"AAyDA,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,oBAAoB,EAC1B,MAAM,4CAA4C,CAAC;AAIpD,wDAAwD;AACxD,eAAO,MAAM,eAAe,WAAW,CAAC;AAExC,4FAA4F;AAC5F,eAAO,MAAM,yBAAyB,gBAAgB,CAAC;AAQvD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAIpH;AAmBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQ3G;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,oBAAoB,EAAE,CAE9F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,eAAe,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClG;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EACb,kBAAkB,EAAE,MAAM,EAC1B,YAAY,EAAE,SAAS,oBAAoB,EAAE,GAC5C,mBAAmB,CAOrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,SAAS,MAAM,EAAE,CAUnB;AAED;sGACsG;AACtG,MAAM,MAAM,0BAA0B,GAClC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,0BAA0B,CA+B5B;AAmBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAChE,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU7B"}
|