@open-everywhere/provider-notion 0.7.0 → 0.8.1
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/agent/channels/notion-api.ts +4 -4
- package/agent/channels/notion-engagement.ts +3 -2
- package/agent/channels/notion-event-reach.ts +36 -0
- package/agent/channels/notion-scope.ts +3 -0
- package/agent/channels/notion-supervisor.ts +3 -3
- package/agent/channels/notion-webhook-timing.ts +4 -2
- package/agent/channels/notion.ts +5 -1
- package/agent/docs-generated.ts +422 -3
- package/agent/register.ts +5 -0
- package/agent/surface-parity.ts +1 -0
- package/agent/tools/read_notion_comments.ts +20 -1
- package/agent/tools/read_notion_page.ts +2 -0
- package/dist/agent/channels/notion-api.d.ts +0 -23
- package/dist/agent/channels/notion-api.d.ts.map +1 -1
- package/dist/agent/channels/notion-api.js +4 -4
- package/dist/agent/channels/notion-api.js.map +1 -1
- package/dist/agent/channels/notion-engagement.d.ts +2 -1
- package/dist/agent/channels/notion-engagement.d.ts.map +1 -1
- package/dist/agent/channels/notion-engagement.js.map +1 -1
- package/dist/agent/channels/notion-event-reach.d.ts +8 -0
- package/dist/agent/channels/notion-event-reach.d.ts.map +1 -0
- package/dist/agent/channels/notion-event-reach.js +39 -0
- package/dist/agent/channels/notion-event-reach.js.map +1 -0
- package/dist/agent/channels/notion-scope.d.ts.map +1 -1
- package/dist/agent/channels/notion-scope.js +3 -1
- package/dist/agent/channels/notion-scope.js.map +1 -1
- package/dist/agent/channels/notion-supervisor.d.ts +0 -28
- package/dist/agent/channels/notion-supervisor.d.ts.map +1 -1
- package/dist/agent/channels/notion-supervisor.js +3 -3
- package/dist/agent/channels/notion-supervisor.js.map +1 -1
- package/dist/agent/channels/notion-webhook-timing.d.ts +3 -2
- package/dist/agent/channels/notion-webhook-timing.d.ts.map +1 -1
- package/dist/agent/channels/notion-webhook-timing.js.map +1 -1
- package/dist/agent/channels/notion.d.ts.map +1 -1
- package/dist/agent/channels/notion.js +5 -2
- package/dist/agent/channels/notion.js.map +1 -1
- package/dist/agent/docs-generated.d.ts.map +1 -1
- package/dist/agent/docs-generated.js +422 -3
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +5 -1
- package/dist/agent/register.js.map +1 -1
- package/dist/agent/surface-parity.d.ts.map +1 -1
- package/dist/agent/surface-parity.js +1 -0
- package/dist/agent/surface-parity.js.map +1 -1
- package/dist/agent/tools/read_notion_comments.d.ts.map +1 -1
- package/dist/agent/tools/read_notion_comments.js +21 -2
- package/dist/agent/tools/read_notion_comments.js.map +1 -1
- package/dist/agent/tools/read_notion_page.d.ts.map +1 -1
- package/dist/agent/tools/read_notion_page.js +2 -0
- package/dist/agent/tools/read_notion_page.js.map +1 -1
- package/package.json +6 -2
|
@@ -34,7 +34,7 @@ import type { NotionParentLink } from "./notion-ancestry-trust.js";
|
|
|
34
34
|
/** Notion REST base + the API version header value. The version is a code constant (OPE-2411):
|
|
35
35
|
* it is a stable version that supports the comment retrieve/create endpoints and rich-text
|
|
36
36
|
* `mention` objects — everything this surface needs — and a version bump is a code change. */
|
|
37
|
-
|
|
37
|
+
const NOTION_API_BASE = "https://api.notion.com";
|
|
38
38
|
const NOTION_API_VERSION = "2022-06-28";
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -606,7 +606,7 @@ export interface NotionBlockParent {
|
|
|
606
606
|
|
|
607
607
|
/** GET /v1/blocks/{block_id} — used ONLY to answer "which page owns this block?" for the domain
|
|
608
608
|
* fence (channels/notion-scope.ts). Throws on a non-2xx response. */
|
|
609
|
-
|
|
609
|
+
async function retrieveBlock(blockId: string, opts: NotionCallOpts): Promise<NotionBlockParent> {
|
|
610
610
|
const res = await notionFetch(`${NOTION_API_BASE}/v1/blocks/${encodeURIComponent(blockId)}`, { method: "GET", headers: notionHeaders(opts) }, opts);
|
|
611
611
|
if (!res.ok) throw new Error(`retrieveBlock ${blockId} failed: ${res.status} ${await safeText(res)}`);
|
|
612
612
|
return (await res.json()) as NotionBlockParent;
|
|
@@ -624,7 +624,7 @@ export async function retrieveBlock(blockId: string, opts: NotionCallOpts): Prom
|
|
|
624
624
|
* runs out of hops, or the lookup throws — every one of which the caller must treat as a REFUSAL,
|
|
625
625
|
* never as permission.
|
|
626
626
|
*/
|
|
627
|
-
|
|
627
|
+
const BLOCK_PARENT_MAX_HOPS = 5;
|
|
628
628
|
export async function resolveBlockOwningPage(blockId: string, opts: NotionCallOpts): Promise<string | undefined> {
|
|
629
629
|
let id = blockId;
|
|
630
630
|
for (let hop = 0; hop < BLOCK_PARENT_MAX_HOPS; hop++) {
|
|
@@ -659,7 +659,7 @@ export interface NotionPageParent {
|
|
|
659
659
|
|
|
660
660
|
/** GET /v1/pages/{page_id} — used ONLY to answer "what owns this page?" for the ancestor walk
|
|
661
661
|
* (channels/notion-ancestry-trust.ts). Throws on a non-2xx response. */
|
|
662
|
-
|
|
662
|
+
async function retrievePage(pageId: string, opts: NotionCallOpts): Promise<NotionPageParent> {
|
|
663
663
|
const res = await notionFetch(`${NOTION_API_BASE}/v1/pages/${encodeURIComponent(pageId)}`, { method: "GET", headers: notionHeaders(opts) }, opts);
|
|
664
664
|
if (!res.ok) throw new Error(`retrievePage ${pageId} failed: ${res.status} ${await safeText(res)}`);
|
|
665
665
|
return (await res.json()) as NotionPageParent;
|
|
@@ -28,6 +28,7 @@ import { buildNotionAuth, notionConversationKey } from "./notion-identity.js";
|
|
|
28
28
|
// TYPE ONLY — it erases, so the "this module imports only notion-identity.ts and core" note above
|
|
29
29
|
// `markNotionEngaged`'s use in notion-supervisor.ts still holds and no cycle is added.
|
|
30
30
|
import type { NotionVia } from "./notion-scope.js";
|
|
31
|
+
import type { AuditDetail, AuditDetailBuilder } from "@open-everywhere/core/agent/adapter-api.js";
|
|
31
32
|
|
|
32
33
|
/** The facts one Notion delivery contributes to the seam — everything the anchor and the partition
|
|
33
34
|
* need, and nothing about the comment's text. */
|
|
@@ -363,8 +364,8 @@ export interface NotionDispatchJoinIds {
|
|
|
363
364
|
* An absent value is OMITTED rather than written as `undefined`: an audit row that carries a key
|
|
364
365
|
* with no value reads, to anyone querying it, like a value that was looked for and found empty.
|
|
365
366
|
*/
|
|
366
|
-
export function notionDispatchJoinDetail(ids: NotionDispatchJoinIds):
|
|
367
|
-
const out:
|
|
367
|
+
export function notionDispatchJoinDetail(ids: NotionDispatchJoinIds): AuditDetail {
|
|
368
|
+
const out: AuditDetailBuilder = {};
|
|
368
369
|
if (ids.deliveryId) out.deliveryId = ids.deliveryId;
|
|
369
370
|
if (ids.commentId) out.commentId = ids.commentId;
|
|
370
371
|
if (ids.wakeId) out.wakeId = ids.wakeId;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// DO NOTION'S EVENTS REACH THIS DEPLOYMENT? (OPE-2547) — Notion's probe for core's routines/event-reach.ts.
|
|
2
|
+
//
|
|
3
|
+
// The native Notion route hands a delivery to the event plane only when an integration's inbound secret
|
|
4
|
+
// VERIFIED it (./notion.ts, the event-plane leg is gated on `verified`). The `webhooks.allowUnverifiedNotion`
|
|
5
|
+
// hatch never fires a routine, so it does not count here. With no integration's inbound secret configured,
|
|
6
|
+
// no Notion routine can fire. This probe resolves each integration's secret through `getOperatorSecret`, the
|
|
7
|
+
// resolver the route uses, and keeps only whether one resolved.
|
|
8
|
+
//
|
|
9
|
+
// 🔴 ONLY "NOT SET" IS A NO. `OperatorSecretNotSetError` says the secret is set nowhere, so the answer is
|
|
10
|
+
// false. Every other failure (an unreadable config, a key that is not armed, a decrypt error) is no answer at
|
|
11
|
+
// all, so the probe THROWS and core reads `unknown`. A false "no" would tell every Notion watch's room that it
|
|
12
|
+
// is dead. One case remains: `getOperatorSecret` falls back to the env var when the credential store is
|
|
13
|
+
// unreachable, so a secret kept only in the store reads as not set during a store outage.
|
|
14
|
+
|
|
15
|
+
import { getConfig, getOperatorSecret, OperatorSecretNotSetError } from "@open-everywhere/core/agent/adapter-api.js";
|
|
16
|
+
import { notionIntegrations } from "./notion-credentials.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True when at least one Notion integration's inbound signing secret resolves. The route admits a delivery signed
|
|
20
|
+
* with ANY key that resolves, so one broken key does not decide: the probe tries every name. It throws only when no
|
|
21
|
+
* key resolved and at least one failed in a way other than "not set".
|
|
22
|
+
*/
|
|
23
|
+
export async function notionEventsReach(get: typeof getOperatorSecret = getOperatorSecret): Promise<boolean> {
|
|
24
|
+
// An unreadable config throws through: no answer, never "no integration".
|
|
25
|
+
const names = [...new Set(notionIntegrations(getConfig().loadDomains()).map((i) => i.inboundSecret))];
|
|
26
|
+
let failure: unknown;
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
try {
|
|
29
|
+
if ((await get(name)).value.length > 0) return true;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (!(err instanceof OperatorSecretNotSetError) && failure === undefined) failure = err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (failure !== undefined) throw failure;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import {
|
|
46
46
|
appendAudit,
|
|
47
|
+
auditSurfaceModeFrom,
|
|
47
48
|
bindFrom,
|
|
48
49
|
domainReachVerdict,
|
|
49
50
|
domainRoster,
|
|
@@ -431,6 +432,8 @@ async function auditNotionReadRefusal(
|
|
|
431
432
|
platform: "notion",
|
|
432
433
|
workspace: foreignCaller ? "unknown" : partition.workspace,
|
|
433
434
|
surface: foreignCaller ? "unknown" : partition.surface,
|
|
435
|
+
// OPE-1940: the mode belongs to the caller's own surface, so a foreign-caller row carries none.
|
|
436
|
+
...(foreignCaller ? {} : auditSurfaceModeFrom(ctx)),
|
|
434
437
|
...(principalId ? { principalId } : {}),
|
|
435
438
|
action: "read.refused",
|
|
436
439
|
detail: {
|
|
@@ -658,8 +658,8 @@ export function notionIngestedMessages(d: {
|
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
/**
|
|
661
|
-
* THE EXTERNAL-AUTHOR DECISION for one Notion dispatch (OPE-507). Pure,
|
|
662
|
-
*
|
|
661
|
+
* THE EXTERNAL-AUTHOR DECISION for one Notion dispatch (OPE-507). Pure, so the three behaviours
|
|
662
|
+
* need no store and no live wake.
|
|
663
663
|
*
|
|
664
664
|
* Notion's three answers, and why they are what they are:
|
|
665
665
|
* • the TRIGGER's author is excluded ⇒ NO WAKE AT ALL (`suppressed`). That covers behaviour 2
|
|
@@ -676,7 +676,7 @@ export function notionIngestedMessages(d: {
|
|
|
676
676
|
* message" into a shared doc is a disclosure, not a notice, so the Notion answer is to do nothing.
|
|
677
677
|
* The surfaces that HAVE a private channel (a Discord DM, a Slack DM) post the notice there.
|
|
678
678
|
*/
|
|
679
|
-
|
|
679
|
+
function notionExternalAuthorDecision(d: {
|
|
680
680
|
readonly trigger: NotionWakeMessage;
|
|
681
681
|
readonly threadComments: readonly NotionWakeMessage[];
|
|
682
682
|
readonly ingested: readonly IngestedChatMessage[];
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// this collector pure and injectable: production records one content-free sample, while a fake
|
|
6
6
|
// clock can prove the attribution without sleeping or calling a service.
|
|
7
7
|
|
|
8
|
+
import type { AuditDetail } from "@open-everywhere/core/agent/adapter-api.js";
|
|
9
|
+
|
|
8
10
|
export const NOTION_WEBHOOK_TIMING_ACTION = "notion.webhook_timing";
|
|
9
11
|
|
|
10
12
|
export type NotionWebhookTimingPhase =
|
|
@@ -17,8 +19,8 @@ export type NotionWebhookTimingPhase =
|
|
|
17
19
|
| "eveSend"
|
|
18
20
|
| "finalAudit";
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
/** An audit detail, so its error-named keys carry the OPE-2549 brand guard. It holds none today. */
|
|
23
|
+
export interface NotionWebhookTimingDetail extends AuditDetail {
|
|
22
24
|
readonly outcome: string;
|
|
23
25
|
readonly failedPhase?: NotionWebhookTimingPhase;
|
|
24
26
|
readonly hmacCredentialMs?: number;
|
package/agent/channels/notion.ts
CHANGED
|
@@ -58,11 +58,13 @@
|
|
|
58
58
|
// notion-ambient-surface-design.md Step 5 / the e2e-issue-surfaces driver investigation).
|
|
59
59
|
// ────────────────────────────────────────────────────────────────────────────────────────
|
|
60
60
|
|
|
61
|
+
import { auditErrorDetail } from "@open-everywhere/core/agent/adapter-api.js";
|
|
61
62
|
import { defineChannel, POST, type RouteHandlerArgs } from "eve/channels";
|
|
62
63
|
import {
|
|
63
64
|
receiptEventTime,
|
|
64
65
|
spreadVendorTs,
|
|
65
66
|
appendAudit,
|
|
67
|
+
auditSurfaceModeFrom,
|
|
66
68
|
ingressSpendGate,
|
|
67
69
|
auditAmbientSkip,
|
|
68
70
|
deploymentOrg,
|
|
@@ -592,7 +594,7 @@ async function deliverNotionAnswer(input: {
|
|
|
592
594
|
{ continuationToken: input.token, context: input.context },
|
|
593
595
|
);
|
|
594
596
|
} catch (err) {
|
|
595
|
-
const error = (err
|
|
597
|
+
const error = auditErrorDetail(err).error;
|
|
596
598
|
log(`answer ${input.answer.requestId} could NOT be delivered (${error}) — falling back to an ordinary wake, the question stays open`);
|
|
597
599
|
await appendAudit({
|
|
598
600
|
org: input.answerOrg,
|
|
@@ -841,6 +843,8 @@ const notionWebhookRouteHandler = timedNotionWebhookRoute(
|
|
|
841
843
|
platform: "notion",
|
|
842
844
|
workspace: d.workspaceId ?? "unknown",
|
|
843
845
|
surface: d.pageId || d.discussionId || "unknown",
|
|
846
|
+
// OPE-1940: the RESOLVED mode, from the same delivery auth the wake binds.
|
|
847
|
+
...auditSurfaceModeFrom({ channel: { kind: "notion" }, session: { auth: { current: d.auth } } } as unknown as Parameters<typeof auditSurfaceModeFrom>[0]),
|
|
844
848
|
principalId: d.auth.principalId || undefined,
|
|
845
849
|
action: "notion.dispatched",
|
|
846
850
|
detail: {
|