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