@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,283 @@
|
|
|
1
|
+
// THE NOTION AMBIENT ENGAGEMENT SEAM (chat-surface contract Β§2.5) β the Notion wiring over core's
|
|
2
|
+
// pure engagement policy (`packages/core/agent/channels/engagement.ts`). It gives Notion the middle
|
|
3
|
+
// posture Slack, Discord and Teams already have: addressing the app marks the PAGE ENGAGED, a later
|
|
4
|
+
// PLAIN comment on that page wakes the agent, and `stand_down` stores the stop.
|
|
5
|
+
//
|
|
6
|
+
// THE ANCHOR is `notionConversationKey(workspaceId, pageId)` β `<workspace>/<page>`, the same string
|
|
7
|
+
// the mapper publishes as `anchorChannelId` for the stop registry. The doc session is PAGE-scoped
|
|
8
|
+
// (`notionSupervisorToken` keys the standing session by page, aware of all the page's discussions),
|
|
9
|
+
// so the page is the conversation a person can address a stop to, and a per-discussion mark would
|
|
10
|
+
// leave the same session answering the discussion next door.
|
|
11
|
+
//
|
|
12
|
+
// π΄ THE ADDRESS IS A NAME-DROP, NOT AN @MENTION, AND THAT IS A VENDOR FACT. Notion lets no user
|
|
13
|
+
// @mention an internal integration at all, so `classifyNotionComment` promotes a name-drop (or the
|
|
14
|
+
// structurally-unreachable structured mention) to `direct` β that IS Notion's addressed leg, and it
|
|
15
|
+
// is therefore what enrols a page here. `notionNameDrop` owns the matching, including its
|
|
16
|
+
// word-boundary guards; this module never looks at comment text.
|
|
17
|
+
import { getOperationalStore, getThreadEngagement, markThreadEngaged, resolvePartition, shouldDeliverReply, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
18
|
+
import { buildNotionAuth, notionConversationKey } from "./notion-identity.js";
|
|
19
|
+
/** The engagement-registry anchor for one Notion delivery, or `undefined` when either id is missing.
|
|
20
|
+
*
|
|
21
|
+
* π΄ IT IS THE ONE PREDICATE THE MAPPER AND THE SEAM SHARE. `bindNotion` publishes exactly this as
|
|
22
|
+
* `engagementKey`, so `stand_down`'s mark and this seam's read address the same row. */
|
|
23
|
+
export function notionEngagementAnchor(f) {
|
|
24
|
+
return notionConversationKey(f.workspaceId, f.pageId);
|
|
25
|
+
}
|
|
26
|
+
/** The partition one page's engagement row lives in β `notion/<workspace>/<page>`, which is what
|
|
27
|
+
* `resolvePartition` gives a bound Notion session and therefore what `stand_down` writes under. It
|
|
28
|
+
* is built through the same `buildNotionAuth` the wake dispatches, so the seam can never key a row
|
|
29
|
+
* the turn would not. Null on an unresolvable identity β the caller then keeps today's path. */
|
|
30
|
+
export function notionSeamPartition(f) {
|
|
31
|
+
if (!f.workspaceId || !f.pageId)
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
const auth = buildNotionAuth({
|
|
35
|
+
workspaceId: f.workspaceId,
|
|
36
|
+
pageId: f.pageId,
|
|
37
|
+
...(f.principalId ? { principalId: f.principalId } : {}),
|
|
38
|
+
...(f.credentialId ? { credentialId: f.credentialId } : {}),
|
|
39
|
+
});
|
|
40
|
+
return resolvePartition({ channel: { kind: "notion" }, session: { auth: { current: auth } } });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the seam for one NON-ADDRESSED comment β READ-ONLY. An addressed comment never reaches
|
|
48
|
+
* this function: it marks instead ({@link markNotionEngaged}), so exactly one caller writes the row.
|
|
49
|
+
*
|
|
50
|
+
* THE DECISION ITSELF IS CORE'S. `shouldDeliverReply` owns the disqualifier order and the reasons;
|
|
51
|
+
* this function only translates Notion facts into its input. Nothing here re-states the policy.
|
|
52
|
+
*
|
|
53
|
+
* FAIL-CLOSED on a store error: `getThreadEngagement` reads a failure as `null`, so an unreadable
|
|
54
|
+
* registry holds rather than delivers.
|
|
55
|
+
*
|
|
56
|
+
* `authorIsBot` is the caller's own loop-guard verdict. The classifier drops a self-authored or
|
|
57
|
+
* integration-authored comment before this runs, so it is `false` on every live call; it stays a
|
|
58
|
+
* parameter rather than a hard-coded literal so the guard is visible at the call site and a test can
|
|
59
|
+
* drive the other direction.
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveNotionEngagement(f, authorIsBot, deps = {}) {
|
|
62
|
+
const anchor = notionEngagementAnchor(f);
|
|
63
|
+
if (!anchor)
|
|
64
|
+
return { kind: "unanchored", reason: "the delivery names no workspace and page β no row to address" };
|
|
65
|
+
const partition = notionSeamPartition(f);
|
|
66
|
+
if (!partition)
|
|
67
|
+
return { kind: "unanchored", reason: "the delivery has no resolvable partition β no row to address" };
|
|
68
|
+
const read = deps.getEngagement ?? ((p, key) => getThreadEngagement(p, key));
|
|
69
|
+
const row = await read(partition, anchor);
|
|
70
|
+
const decision = shouldDeliverReply({
|
|
71
|
+
isEngaged: row !== null,
|
|
72
|
+
stopped: row?.stopped ?? false,
|
|
73
|
+
authorIsBot,
|
|
74
|
+
isAppMention: false, // an addressed comment never reaches this function
|
|
75
|
+
inThread: true, // an anchored Notion page IS the container engagement is kept per
|
|
76
|
+
});
|
|
77
|
+
if (decision.deliver)
|
|
78
|
+
return { kind: "deliver", anchor, reason: decision.reason };
|
|
79
|
+
return { kind: "hold", anchor, reason: decision.reason, stopped: row?.stopped === true };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Mark one page ENGAGED on an addressed (name-drop or structured-mention) comment. ONCE PER COMMENT.
|
|
83
|
+
*
|
|
84
|
+
* `participating: true` is the re-invitation: it promotes the row and CLEARS a previous stand-down,
|
|
85
|
+
* which is what "name the agent to bring it back" means here.
|
|
86
|
+
*
|
|
87
|
+
* π΄ THE MARK IS DEDUPED, AND THE STOP IS WHY β the same durable guard GitHub's and Linear's marks
|
|
88
|
+
* take. Clearing the flag is right for a person's second name-drop and WRONG for Notion redelivering
|
|
89
|
+
* the first one. THE ROUTE DOES NOT ALWAYS ACK, so the redelivery is reachable: `handleNotionDelivery`
|
|
90
|
+
* carries no top-level `catch`, and eve's channel dispatcher answers a throw with a 500 β while this
|
|
91
|
+
* mark sits ABOVE `getOrCreateSession` and the wake, either of which can throw on a store outage. The
|
|
92
|
+
* sequence that costs is then: name-drop marks the page, the mint throws, Notion is told 500, somebody
|
|
93
|
+
* asks the agent to stand down, and Notion's retry of the original comment lifts the stop.
|
|
94
|
+
* `commentId` is the trigger comment's own id, so the guard keys on the thing that must happen once.
|
|
95
|
+
* DURABLE layer only, and it expires with the store's TTL β a retry later than that window can still
|
|
96
|
+
* re-mark. FAIL-OPEN on a store error, the posture every dedupe in this repository takes.
|
|
97
|
+
*
|
|
98
|
+
* Best-effort throughout β `markThreadEngaged` swallows a store failure, because engagement
|
|
99
|
+
* bookkeeping must never fail a turn. Returns the anchor it marked, or null when it marked nothing.
|
|
100
|
+
*/
|
|
101
|
+
export async function markNotionEngaged(f, commentId, deps = {}) {
|
|
102
|
+
const anchor = notionEngagementAnchor(f);
|
|
103
|
+
if (!anchor)
|
|
104
|
+
return null;
|
|
105
|
+
const partition = notionSeamPartition(f);
|
|
106
|
+
if (!partition)
|
|
107
|
+
return null;
|
|
108
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
109
|
+
const atIso = now();
|
|
110
|
+
if (commentId !== undefined && commentId.length > 0) {
|
|
111
|
+
const seen = deps.markEventSeen ?? liveMarkEngageSeen;
|
|
112
|
+
if (!(await seen(partition.org, `notion-engage:${anchor}:${commentId}`, atIso)))
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const mark = deps.markEngaged ?? ((p, key, at) => markThreadEngaged(p, key, at, { participating: true }));
|
|
116
|
+
await mark(partition, anchor, atIso);
|
|
117
|
+
return anchor;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* ONE WAKE PER COMMENT (OPE-1906). TRUE = this comment has not woken the agent inside the window,
|
|
121
|
+
* so the caller should dispatch. FALSE = the same comment id already did, so the caller must not.
|
|
122
|
+
*
|
|
123
|
+
* π΄ THE OTHER HALF OF THE LOOP GUARD, AND THE CLASSIFIER CANNOT BE IT. `classifyNotionComment`
|
|
124
|
+
* reads one comment's author; a REDELIVERY of a person's comment carries the same author and the
|
|
125
|
+
* same text, so it classifies identically and dispatches again β a second model turn, a second
|
|
126
|
+
* `notion.dispatched` row, and a second reply into the discussion. Nothing else on this road is
|
|
127
|
+
* keyed on the delivery, so this is the only place that can say "again".
|
|
128
|
+
*
|
|
129
|
+
* REDELIVERY IS REACHABLE, and {@link markNotionEngaged} above already says why: the route does not
|
|
130
|
+
* always ack. `handleNotionDelivery` carries no top-level `catch`, and eve's channel dispatcher
|
|
131
|
+
* answers a throw with a 500, so Notion retries. A page shared with two subscriptions delivers the
|
|
132
|
+
* same comment twice by design (OPE-467), and a vendor retry needs no failure of ours at all.
|
|
133
|
+
*
|
|
134
|
+
* π΄ THE MARK IS TAKEN BEFORE THE WAKE, AND THAT TRADE IS DELIBERATE. Marking afterwards would let
|
|
135
|
+
* two concurrent deliveries of one comment both run a turn, which is the cost this guard exists to
|
|
136
|
+
* stop; `markEventSeen` is atomic across instances, so marking first stops them. The price is that
|
|
137
|
+
* a delivery whose dispatch THROWS is not retried inside the window β the same price the engage
|
|
138
|
+
* mark one step above already pays, and the reason this guard sits as LATE as it can, below every
|
|
139
|
+
* cheaper refusal, so only a delivery that was really about to wake a turn ever spends a mark.
|
|
140
|
+
*
|
|
141
|
+
* π΄ A SUITE THAT DRIVES `handleNotionDelivery` PAST THIS POINT MUST INJECT `markEventSeen`, or
|
|
142
|
+
* pin its own store path. The default is the DURABLE operational store, and an unpinned suite in
|
|
143
|
+
* this package shares one sqlite file (scripts/suite-store-isolation-lint.ts says why) β so a suite
|
|
144
|
+
* that replays a fixed comment id writes a mark that survives the run, and its NEXT run reads
|
|
145
|
+
* `duplicate-delivery` where it expected `dispatched`. `lint:suite-store` cannot catch that: its
|
|
146
|
+
* reach test is a source scan, and such a suite names no store entry point of its own. Five suites
|
|
147
|
+
* inject the stub today.
|
|
148
|
+
*
|
|
149
|
+
* FAIL-OPEN, three ways: a store error, an unresolvable partition and a delivery with no comment id
|
|
150
|
+
* all answer TRUE, so an outage costs a duplicate turn rather than a deaf deployment. The try/catch
|
|
151
|
+
* covers the INJECTED edge too, not just the live default (the shape webhooks/replay-dedupe.ts and
|
|
152
|
+
* webhooks/reject-audit.ts take) β which is how a test drives the fail-open arm without breaking a
|
|
153
|
+
* real store. `commentId` is the thing that must happen once, and a Notion comment id is globally
|
|
154
|
+
* unique, so the key needs no anchor.
|
|
155
|
+
*/
|
|
156
|
+
export async function firstNotionDispatchOfComment(f, commentId, deps = {}) {
|
|
157
|
+
if (commentId === undefined || commentId.length === 0)
|
|
158
|
+
return true;
|
|
159
|
+
const partition = notionSeamPartition(f);
|
|
160
|
+
if (!partition)
|
|
161
|
+
return true;
|
|
162
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
163
|
+
const seen = deps.markEventSeen ?? liveMarkDispatchSeen;
|
|
164
|
+
try {
|
|
165
|
+
return await seen(partition.org, `notion-dispatch:${commentId}`, now());
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* WHAT WOKE ONE ADMITTED DELIVERY, in the wake envelope's own vocabulary (core's `WakeAddressing`).
|
|
173
|
+
* It is the value the `notion.dispatched` audit row reports, and NO PRODUCT CODE BRANCHES ON IT.
|
|
174
|
+
* Every reader is an EVIDENCE reader outside the product: the journey pack's `reportedThreadFollow`
|
|
175
|
+
* (packages/core/scripts/e2e/engaged-rows.ts) gates `J-ENGAGED-notion`'s follow-up step on it,
|
|
176
|
+
* `drive-notion-namedrop.ts` prints and asserts it, and the test tiers read it. So changing the
|
|
177
|
+
* value moves what the pack can prove; it moves no delivery decision.
|
|
178
|
+
*
|
|
179
|
+
* π΄ IT IS DELIBERATELY NOT THE CLASSIFIER'S VALUE. The classifier answers one question β did this
|
|
180
|
+
* comment ADDRESS the agent β and an engaged follow-up did not, so it stays `channel-watch`. The
|
|
181
|
+
* supervisor core needs that: `channel-watch` is what makes the turn a JUDGED one, and promoting it
|
|
182
|
+
* would silence the very wake this seam exists to admit.
|
|
183
|
+
*
|
|
184
|
+
* π΄ BUT `channel-watch` ALSO NAMES UNOWNED ROOM CHATTER on an ambient page, and the audit row is
|
|
185
|
+
* the one place those two must not read alike. A row that reported `channel-watch` for both cannot
|
|
186
|
+
* tell an ENGAGED wake from an AMBIENT one, and that distinction is the whole evidentiary worth of
|
|
187
|
+
* the Β§2.5 seam on this surface: it is what `J-ENGAGED-notion` gates its follow-up step on
|
|
188
|
+
* (`reportedThreadFollow`, packages/core/scripts/e2e/engaged-rows.ts). Until OPE-1501 the row was
|
|
189
|
+
* unreachable on an addressed-only page β the ambient gate returned before the classifier ran β so
|
|
190
|
+
* the collision cost nothing. The gate now admits an engaged follow-up, and the collision costs the
|
|
191
|
+
* proof, which is what OPE-1510 fixed here.
|
|
192
|
+
*
|
|
193
|
+
* π΄ IT REPORTS THE REGISTRY, NOT THE DOOR. The seam is read for every plain comment, above and
|
|
194
|
+
* independently of the ambient gate, so on an ambient-ENABLED page a plain comment on an engaged
|
|
195
|
+
* page also reads `thread-follow` β the ambient flag would have admitted it either way. The value
|
|
196
|
+
* therefore separates an engaged wake from an ambient one only where the domain has ambient OFF,
|
|
197
|
+
* which is the addressed-only posture the journeys drive.
|
|
198
|
+
*
|
|
199
|
+
* `engagedFollowUp` is the webhook core's own seam verdict, and the `channel-watch` guard below is
|
|
200
|
+
* belt-and-braces: an ADDRESSED comment never asks the seam, so it can never carry the flag.
|
|
201
|
+
*/
|
|
202
|
+
export function notionWakeAddressing(addressing, engagedFollowUp) {
|
|
203
|
+
return addressing === "channel-watch" && engagedFollowUp === true ? "thread-follow" : addressing;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* THE SEAM'S WHOLE CONTRIBUTION TO THE AUDIT ROW, as one pure mapping.
|
|
207
|
+
*
|
|
208
|
+
* π΄ THE TWO FIELDS ANSWER DIFFERENT QUESTIONS, AND A ROW NEEDS BOTH. `addressing` says WHAT WOKE
|
|
209
|
+
* the delivery. `via` says HOW THE OPERATOR DECLARED THE PAGE, which is the domain origin the
|
|
210
|
+
* addressed door gated on (`notionPageDomain`, channels/notion-scope.ts). A `container` claim
|
|
211
|
+
* reaches pages NOBODY ENUMERATED β the whole workspace, or a same-level page glob, which are the
|
|
212
|
+
* two shapes a container entry may take (memory/domain.ts); a `roster` entry is a page an
|
|
213
|
+
* operator wrote down. Both reach the same door, and a row carrying only `addressing` reports them
|
|
214
|
+
* identically β which leaves the wider admission looking exactly like the narrower one in the
|
|
215
|
+
* audit trail. The row says WHICH KIND of claim, never how wide that claim is: read the domain's
|
|
216
|
+
* own container entry for the breadth.
|
|
217
|
+
*
|
|
218
|
+
* `via` is the webhook core's own read, carried on the dispatch. An absent field reads `none`: the
|
|
219
|
+
* conservative word, so a caller that omits it can never claim a declaration it did not make.
|
|
220
|
+
*/
|
|
221
|
+
export function notionWakeAuditDetail(addressing, engagedFollowUp, via) {
|
|
222
|
+
return { addressing: notionWakeAddressing(addressing, engagedFollowUp), via: via ?? "none" };
|
|
223
|
+
}
|
|
224
|
+
/** How long this module remembers one comment id β the window both durable guards below key on.
|
|
225
|
+
* It bounds the vendor retry both of them answer, so ONE number serves both: a redelivery this
|
|
226
|
+
* module treats as "again" for the mark must be "again" for the wake too, or the two guards would
|
|
227
|
+
* disagree about the same delivery. */
|
|
228
|
+
const NOTION_DEDUPE_TTL_MS = 10 * 60_000;
|
|
229
|
+
/** The durable half of the mark dedupe. FAIL-OPEN: an unreachable store answers "not seen", so a
|
|
230
|
+
* store outage costs a redundant mark rather than a lost engagement. */
|
|
231
|
+
let warnedEngageDedupe = false;
|
|
232
|
+
async function liveMarkEngageSeen(org, key, atIso) {
|
|
233
|
+
try {
|
|
234
|
+
return await getOperationalStore().markEventSeen(org, key, atIso, NOTION_DEDUPE_TTL_MS);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (!warnedEngageDedupe) {
|
|
238
|
+
warnedEngageDedupe = true;
|
|
239
|
+
console.warn(`[open-everywhere:notion-engagement] engage dedupe unavailable β a redelivered name-drop may re-mark: ${String(err)}`);
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** The durable half of the WAKE once-guard ({@link firstNotionDispatchOfComment}). Same window as
|
|
245
|
+
* the mark above, and the same FAIL-OPEN posture: an unreachable store costs a duplicate turn,
|
|
246
|
+
* never a silenced comment. */
|
|
247
|
+
let warnedDispatchDedupe = false;
|
|
248
|
+
async function liveMarkDispatchSeen(org, key, atIso) {
|
|
249
|
+
try {
|
|
250
|
+
return await getOperationalStore().markEventSeen(org, key, atIso, NOTION_DEDUPE_TTL_MS);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (!warnedDispatchDedupe) {
|
|
254
|
+
warnedDispatchDedupe = true;
|
|
255
|
+
console.warn(`[open-everywhere:notion-engagement] dispatch dedupe unavailable β a redelivered comment may wake a second turn: ${String(err)}`);
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Build the join ids for the row. Pure, and EVERY FIELD IS AN ID β never a comment body, an author
|
|
262
|
+
* name or a page title.
|
|
263
|
+
*
|
|
264
|
+
* π΄ WHY THE ROW NEEDS THEM. Before this, `notion.dispatched` recorded the discussion and the
|
|
265
|
+
* addressing but nothing that identified the DELIVERY, so a leg that forged one replay could only
|
|
266
|
+
* scan a time window for a verdict row and hope no other wake fell inside it. S-TAINT-DRYRUN Arm B
|
|
267
|
+
* did exactly that and the scan could not tell its own replay's verdict from an ambient one
|
|
268
|
+
* (OPE-1907). With the delivery id on the row, one forge joins one wake joins one session.
|
|
269
|
+
*
|
|
270
|
+
* An absent value is OMITTED rather than written as `undefined`: an audit row that carries a key
|
|
271
|
+
* with no value reads, to anyone querying it, like a value that was looked for and found empty.
|
|
272
|
+
*/
|
|
273
|
+
export function notionDispatchJoinDetail(ids) {
|
|
274
|
+
const out = {};
|
|
275
|
+
if (ids.deliveryId)
|
|
276
|
+
out.deliveryId = ids.deliveryId;
|
|
277
|
+
if (ids.commentId)
|
|
278
|
+
out.commentId = ids.commentId;
|
|
279
|
+
if (ids.wakeId)
|
|
280
|
+
out.wakeId = ids.wakeId;
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=notion-engagement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notion-engagement.js","sourceRoot":"","sources":["../../../agent/channels/notion-engagement.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { type Domain, type EventNormalizer, type EventScopeGrant, type NormalizedEvent, type ScopeStep } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
/**
|
|
3
|
+
* The registry key for BOTH halves of the event plane, and it is the SAME string the webhook
|
|
4
|
+
* verifier registers under β one contract, spelled once. (Since OPE-198 S2 this key is the
|
|
5
|
+
* delivery's SCHEME; on the native Notion route the scheme and the hook name coincide.)
|
|
6
|
+
*/
|
|
7
|
+
export declare const NOTION_EVENT_SOURCE = "notion";
|
|
8
|
+
/** Event-type families that are WORK β routed into the event plane. Prefix-matched on the
|
|
9
|
+
* verbatim `type` discriminator (`page.properties_updated`, `data_source.schema_updated`, β¦). */
|
|
10
|
+
export declare const NOTION_STRUCTURAL_PREFIXES: readonly string[];
|
|
11
|
+
/** Event-type families that are CONVERSATION β the ambient supervisor's, never a routine's.
|
|
12
|
+
* Named explicitly (rather than falling into the unrecognised bucket) so the partition is a
|
|
13
|
+
* reviewed statement in code and a reader can see that the omission is deliberate. */
|
|
14
|
+
export declare const NOTION_CONVERSATIONAL_PREFIXES: readonly string[];
|
|
15
|
+
/** Notion's webhook envelope, probed defensively β a mis-shaped payload must yield `null`, never a
|
|
16
|
+
* throw (a normalizer that throws is caught by `normalizeDelivery`, but an ingress should not rely
|
|
17
|
+
* on that to avoid a 500). Mirrors `notion-api.ts`'s loose typing of the same envelope. */
|
|
18
|
+
export interface NotionEventEnvelope {
|
|
19
|
+
readonly id?: unknown;
|
|
20
|
+
readonly timestamp?: unknown;
|
|
21
|
+
readonly workspace_id?: unknown;
|
|
22
|
+
readonly type?: unknown;
|
|
23
|
+
readonly event_type?: unknown;
|
|
24
|
+
readonly authors?: unknown;
|
|
25
|
+
readonly entity?: {
|
|
26
|
+
readonly id?: unknown;
|
|
27
|
+
readonly type?: unknown;
|
|
28
|
+
};
|
|
29
|
+
readonly data?: {
|
|
30
|
+
readonly parent?: {
|
|
31
|
+
readonly id?: unknown;
|
|
32
|
+
readonly type?: unknown;
|
|
33
|
+
};
|
|
34
|
+
readonly updated_blocks?: unknown;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Is this event type WORK (routable), CONVERSATION (the supervisor's), or unrecognised? Exported
|
|
38
|
+
* so the disjointness can be asserted directly rather than inferred from `normalize` returning
|
|
39
|
+
* null for two different reasons. */
|
|
40
|
+
export declare function notionEventClass(type: string | undefined): "structural" | "conversational" | "unrecognised";
|
|
41
|
+
/**
|
|
42
|
+
* The delivery id Notion carries for one event β its top-level `id`, which is in the BODY (Notion
|
|
43
|
+
* publishes no delivery-id header). THE supported way for the Notion ingress to populate
|
|
44
|
+
* {@link TriggerDelivery.deliveryId}, which is what arms `admitTrigger`'s durable replay dedupe.
|
|
45
|
+
* Returns undefined for an unparseable body and for a blank id alike β both are "this delivery has
|
|
46
|
+
* no id", which the plane already models as not-deduped rather than dropped.
|
|
47
|
+
*/
|
|
48
|
+
export declare function notionEventDeliveryId(rawBody: string): string | undefined;
|
|
49
|
+
/** The scope path one envelope proves. Exported for the unit test, which drives it directly. */
|
|
50
|
+
export declare function notionScopePath(ev: NotionEventEnvelope): ScopeStep[];
|
|
51
|
+
/** Lift one Notion envelope onto the v2 event model, or null when it is not ours to route.
|
|
52
|
+
* Exported for the unit test, which drives it directly rather than through the registry. */
|
|
53
|
+
export declare function liftNotionEvent(ev: NotionEventEnvelope, declaredSource: string): NormalizedEvent | null;
|
|
54
|
+
export declare const notionEventNormalizer: EventNormalizer;
|
|
55
|
+
/**
|
|
56
|
+
* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
57
|
+
* π΄ NOTION'S READ GRANT β AND THE BOUNDARY IT IS *NOT*.
|
|
58
|
+
* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
59
|
+
*
|
|
60
|
+
* WHAT OE ACTUALLY HOLDS, MEASURED (a design note Β§1, Β§6; `notion-api.ts`
|
|
61
|
+
* `resolveNotionCredential`): an INTERNAL INTEGRATION SECRET (`db:notion:channel-key`, broker `db`) β
|
|
62
|
+
* one for the whole deployment by default, or one per domain that binds its own (OPE-467). Each is
|
|
63
|
+
* bearer-token-flat: there is no scopes string, it is bound to a single workspace at creation, and
|
|
64
|
+
* what it can see is a SHARE LIST maintained on NOTION'S side, page by page, by workspace members.
|
|
65
|
+
*
|
|
66
|
+
* π΄ THEREFORE THE CREDENTIAL CANNOT BE THE BOUNDARY, AND THIS ADAPTER DOES NOT PRETEND IT IS.
|
|
67
|
+
* Three properties make that unavoidable, and each one alone would be enough:
|
|
68
|
+
* 1. A domain need not bind its own integration, and by default none does β ten domains then
|
|
69
|
+
* reach Notion with the same bytes, so nothing credential-derived can differ between them.
|
|
70
|
+
* A grant rule that held only for BOUND domains would be a rule with a config-shaped hole.
|
|
71
|
+
* 2. The share list is invisible to OE β it is not in any config we hold and there is no API that
|
|
72
|
+
* would let a PURE, SYNC `grantsRead` consult it even if it were.
|
|
73
|
+
* 3. `EffectiveAccess` has no Notion-shaped granted resource to express it over (`repos` is a
|
|
74
|
+
* hardcoded GitHub `string[]`), and inventing one for a credential nobody varies per domain
|
|
75
|
+
* would be schema for a distinction that does not exist.
|
|
76
|
+
* A `grantsRead: () => true` with those facts in a comment is the failure class this tree has a
|
|
77
|
+
* name for β a documented absence of a boundary that LOOKS like a boundary β so it is not what
|
|
78
|
+
* ships here.
|
|
79
|
+
*
|
|
80
|
+
* WHAT *IS* A REAL, PER-DOMAIN, DEPLOYER-OWNED NOTION BOUNDARY: which domain a Notion surface
|
|
81
|
+
* belongs to. `domainsForSurface` reads the static `domains` registry in the app tree β deployer
|
|
82
|
+
* config, merged by PR, validated at module load, with a surface belonging to at most ONE domain.
|
|
83
|
+
* It is the SAME authority, over the SAME `notion/<workspace>/<page>` surface key, that already
|
|
84
|
+
* gates whether the ambient supervisor may engage on that page at all (`notion.ts`
|
|
85
|
+
* `notionAmbientEnabled`). So the rule is:
|
|
86
|
+
*
|
|
87
|
+
* A routine may be triggered by a Notion event iff the routine's partition is in a domain the
|
|
88
|
+
* deployer put the affected Notion surface in.
|
|
89
|
+
*
|
|
90
|
+
* It REFUSES things that are real: a routine in domain `payments` cannot subscribe to a page the
|
|
91
|
+
* deployer put in `security`; a page in NO domain cannot trigger anything anywhere; a delivery
|
|
92
|
+
* proving only a workspace is refused outright, because a workspace is not a surface anybody
|
|
93
|
+
* configured.
|
|
94
|
+
*
|
|
95
|
+
* INNERMOST-CONFIGURED-WINS, AND IT NEVER FALLS THROUGH A REFUSAL. The walk goes inwardsβoutwards
|
|
96
|
+
* and the FIRST step the registry knows about is authoritative: it grants or it refuses, and the
|
|
97
|
+
* walk stops. A step the registry says nothing about is skipped (deployers configure pages and
|
|
98
|
+
* databases, not every descendant), which is what makes the rule usable β but continuing PAST a
|
|
99
|
+
* refusal would let a parent-database grant authorize a child page the deployer deliberately put
|
|
100
|
+
* somewhere else, which is the widening this ordering exists to prevent.
|
|
101
|
+
*
|
|
102
|
+
* β οΈ TWO ASSUMPTIONS, STATED BECAUSE THEY ARE THE ONES MOST LIKELY TO SHIFT.
|
|
103
|
+
* β’ `loadDomains` is explicitly a swappable seam ("the seam a DB/dashboard would swap behind",
|
|
104
|
+
* memory/config/domains.ts). This grant's authority class is TIED to that seam staying deployer-owned:
|
|
105
|
+
* the day a dashboard lets a non-deployer edit domain membership, this rule silently weakens to
|
|
106
|
+
* whatever authorizes that editor.
|
|
107
|
+
* β’ Notion's own share list is a SECOND fence underneath this one, not a substitute for it: a
|
|
108
|
+
* subscription only delivers events for objects the connection was shared onto, so a delivery
|
|
109
|
+
* arriving at all already proves OE can see the object. That fact is OE-wide, never per-domain,
|
|
110
|
+
* which is exactly why it cannot be the grant.
|
|
111
|
+
*
|
|
112
|
+
* β οΈ IT READS CONFIG, WHICH GITHUB'S GRANT DOES NOT NEED TO. `grantsRead` is documented PURE β no
|
|
113
|
+
* I/O, no ctx, no fetch β and this stays inside that: `getConfig()` is a registry read and
|
|
114
|
+
* `domainsForSurface` reads static, module-load-validated config. It is the SAME read
|
|
115
|
+
* `requestForPartition` (event-authz.ts) already performs on this exact fire path, so it introduces
|
|
116
|
+
* no new authority class and no new I/O. An unregistered config throws, `checkEventReadGrant`
|
|
117
|
+
* catches it as `unresolvable`, and the delivery fails CLOSED.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* The scope steps that can DECIDE a Notion delivery, innermost first. The first step any domain owns
|
|
121
|
+
* is authoritative β it grants or it refuses, and the walk stops there.
|
|
122
|
+
*
|
|
123
|
+
* β ONE LIST, TWO READERS, ON PURPOSE. {@link notionEventScopeGrant} walks it to find the step that
|
|
124
|
+
* AUTHORIZES, and {@link notionTriggerScopeVerdict} walks it to find the step it must check the
|
|
125
|
+
* SIGNER against. If the two walked different orders, the fence would check one step and the grant
|
|
126
|
+
* would authorize another, which is the exact shape of a fence that looks applied and is not.
|
|
127
|
+
*
|
|
128
|
+
* π΄ `tenant` IS DELIBERATELY ABSENT. The grant's own comment holds the standing reason; the fence
|
|
129
|
+
* inherits it, and inherits the consequence too β a forged `workspace_id` is never checked directly.
|
|
130
|
+
* It does not need to be. The tenant only changes WHICH domain a step below resolves to, and the
|
|
131
|
+
* fence then reads that domain's credential.
|
|
132
|
+
*/
|
|
133
|
+
export declare const NOTION_GRANT_STEPS: readonly ["sub", "entity", "container", "group"];
|
|
134
|
+
export declare const notionEventScopeGrant: EventScopeGrant;
|
|
135
|
+
/** {@link notionTriggerScopeVerdict}'s answer. `ok` is the ONLY field a caller may branch on; the
|
|
136
|
+
* `reason` is operator-facing prose for the log line and carries no control flow. */
|
|
137
|
+
export type NotionTriggerScopeVerdict = {
|
|
138
|
+
readonly ok: true;
|
|
139
|
+
readonly detail: string;
|
|
140
|
+
} | {
|
|
141
|
+
readonly ok: false;
|
|
142
|
+
readonly reason: string;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
146
|
+
* π΄ THE INGRESS FENCE (OPE-500) β MAY THIS SIGNER ROUTE A DELIVERY ABOUT THIS SCOPE?
|
|
147
|
+
* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
148
|
+
*
|
|
149
|
+
* `channels/notion.ts` calls this on every VERIFIED structural delivery, BEFORE `admitTrigger`. A
|
|
150
|
+
* refusal drops the delivery out of the trigger plane. It never affects the ambient comment path,
|
|
151
|
+
* which has its own OPE-496 resolution and never reaches this function.
|
|
152
|
+
*
|
|
153
|
+
* THE RULE, in one sentence:
|
|
154
|
+
*
|
|
155
|
+
* A delivery may arm a routine on a scope only when the domain that owns that scope is a domain
|
|
156
|
+
* the SIGNING integration serves.
|
|
157
|
+
*
|
|
158
|
+
* WHY THAT IS THE RIGHT AUTHORITY. The signing integration is the ONE authenticated fact a delivery
|
|
159
|
+
* carries (`gateNotionDeliveryAcross` β the secret that verifies identifies the sender). The scope
|
|
160
|
+
* path is not authenticated at all. `notion-credentials.ts` already establishes which integration a
|
|
161
|
+
* domain binds, and that binding is deployer config merged by PR. So this asks the payload's claim
|
|
162
|
+
* one question the payload cannot answer for itself, and answers it from config.
|
|
163
|
+
*
|
|
164
|
+
* WORKED THROUGH, because the point is what it REFUSES. Domain `payments` binds integration
|
|
165
|
+
* `notion-payments` with its own inbound secret. A holder of `notion-payments`'s `verification_token`
|
|
166
|
+
* signs a body naming a `security` page. The granting step resolves to `security`, whose credential
|
|
167
|
+
* is `notion-security` (or the org default). That is not the signer, and the two do not share an
|
|
168
|
+
* inbound secret, so the delivery routes nothing.
|
|
169
|
+
*
|
|
170
|
+
* π΄ THE ONE THING IT CANNOT SEPARATE, STATED PLAINLY. Two integrations that share an INBOUND SECRET
|
|
171
|
+
* are indistinguishable to the HMAC gate β `connectorIntegrations` defaults a domain's
|
|
172
|
+
* `inboundSecret` to the deployment default, so a domain that binds a credential WITHOUT declaring
|
|
173
|
+
* its own inbound secret signs exactly like the org default. This function accepts that case rather
|
|
174
|
+
* than refusing it, because refusing would break a supported config in which the domain runs no
|
|
175
|
+
* subscription of its own. The honest statement of the property is therefore:
|
|
176
|
+
*
|
|
177
|
+
* A domain is fenced from another domain's forged deliveries IFF it runs its own inbound secret.
|
|
178
|
+
*
|
|
179
|
+
* That is the same condition `notion-credentials.ts` already states for the read boundary, and the
|
|
180
|
+
* runbook already tells operators to satisfy it. It is not a hole this function opens; it is the
|
|
181
|
+
* limit of what the HMAC can tell one sender from another.
|
|
182
|
+
*
|
|
183
|
+
* FAIL CLOSED, AND THE REFUSALS ARE REAL. No signer (the dev hatch), an unparseable body, and a path
|
|
184
|
+
* no domain owns all refuse. The last one costs nothing: {@link notionEventScopeGrant} would refuse
|
|
185
|
+
* the same delivery a moment later, so dropping it here only makes the refusal legible.
|
|
186
|
+
*
|
|
187
|
+
* PURE. `resolveSurface` defaults to the same `getConfig().domainsForSurface` the grant reads, and is
|
|
188
|
+
* injectable so the test drives every arm with no registry.
|
|
189
|
+
*/
|
|
190
|
+
export declare function notionTriggerScopeVerdict(rawBody: string, signerCredentialId: string | undefined, domains: readonly Domain[], resolveSurface?: (p: {
|
|
191
|
+
workspaceKey: string;
|
|
192
|
+
surface: string;
|
|
193
|
+
}) => readonly string[]): NotionTriggerScopeVerdict;
|
|
194
|
+
//# sourceMappingURL=notion-event-normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notion-event-normalizer.d.ts","sourceRoot":"","sources":["../../../agent/channels/notion-event-normalizer.ts"],"names":[],"mappings":"AAmHA,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,SAAS,EAEf,MAAM,4CAA4C,CAAC;AAKpD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,WAAwB,CAAC;AAEzD;kGACkG;AAClG,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAA2C,CAAC;AAEpG;;uFAEuF;AACvF,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAAiB,CAAC;AAE9E;;4FAE4F;AAC5F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrE,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC;YAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;QACrE,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;KACnC,CAAC;CACH;AAsBD;;sCAEsC;AACtC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,gBAAgB,GAAG,cAAc,CAK3G;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEzE;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,EAAE,EAAE,mBAAmB,GAAG,SAAS,EAAE,CAiCpE;AAED;6FAC6F;AAC7F,wBAAgB,eAAe,CAAC,EAAE,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAwDvG;AAED,eAAO,MAAM,qBAAqB,EAAE,eA6BnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,YAAI,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAU,CAAC;AAEnF,eAAO,MAAM,qBAAqB,EAAE,eAwCnC,CAAC;AAkBF;sFACsF;AACtF,MAAM,MAAM,yBAAyB,GACjC;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,cAAc,GAAE,CAAC,CAAC,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAK,SAAS,MAAM,EAA4C,GAC5H,yBAAyB,CA6D3B"}
|