@open-everywhere/provider-notion 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/agent/channels/notion-api.ts +4 -4
  2. package/agent/channels/notion-engagement.ts +3 -2
  3. package/agent/channels/notion-event-reach.ts +36 -0
  4. package/agent/channels/notion-scope.ts +3 -0
  5. package/agent/channels/notion-supervisor.ts +3 -3
  6. package/agent/channels/notion-webhook-timing.ts +4 -2
  7. package/agent/channels/notion.ts +5 -1
  8. package/agent/docs-generated.ts +422 -3
  9. package/agent/register.ts +5 -0
  10. package/agent/surface-parity.ts +1 -0
  11. package/agent/tools/read_notion_comments.ts +20 -1
  12. package/agent/tools/read_notion_page.ts +2 -0
  13. package/dist/agent/channels/notion-api.d.ts +0 -23
  14. package/dist/agent/channels/notion-api.d.ts.map +1 -1
  15. package/dist/agent/channels/notion-api.js +4 -4
  16. package/dist/agent/channels/notion-api.js.map +1 -1
  17. package/dist/agent/channels/notion-engagement.d.ts +2 -1
  18. package/dist/agent/channels/notion-engagement.d.ts.map +1 -1
  19. package/dist/agent/channels/notion-engagement.js.map +1 -1
  20. package/dist/agent/channels/notion-event-reach.d.ts +8 -0
  21. package/dist/agent/channels/notion-event-reach.d.ts.map +1 -0
  22. package/dist/agent/channels/notion-event-reach.js +39 -0
  23. package/dist/agent/channels/notion-event-reach.js.map +1 -0
  24. package/dist/agent/channels/notion-scope.d.ts.map +1 -1
  25. package/dist/agent/channels/notion-scope.js +3 -1
  26. package/dist/agent/channels/notion-scope.js.map +1 -1
  27. package/dist/agent/channels/notion-supervisor.d.ts +0 -28
  28. package/dist/agent/channels/notion-supervisor.d.ts.map +1 -1
  29. package/dist/agent/channels/notion-supervisor.js +3 -3
  30. package/dist/agent/channels/notion-supervisor.js.map +1 -1
  31. package/dist/agent/channels/notion-webhook-timing.d.ts +3 -2
  32. package/dist/agent/channels/notion-webhook-timing.d.ts.map +1 -1
  33. package/dist/agent/channels/notion-webhook-timing.js.map +1 -1
  34. package/dist/agent/channels/notion.d.ts.map +1 -1
  35. package/dist/agent/channels/notion.js +5 -2
  36. package/dist/agent/channels/notion.js.map +1 -1
  37. package/dist/agent/docs-generated.d.ts.map +1 -1
  38. package/dist/agent/docs-generated.js +422 -3
  39. package/dist/agent/docs-generated.js.map +1 -1
  40. package/dist/agent/register.d.ts.map +1 -1
  41. package/dist/agent/register.js +5 -1
  42. package/dist/agent/register.js.map +1 -1
  43. package/dist/agent/surface-parity.d.ts.map +1 -1
  44. package/dist/agent/surface-parity.js +1 -0
  45. package/dist/agent/surface-parity.js.map +1 -1
  46. package/dist/agent/tools/read_notion_comments.d.ts.map +1 -1
  47. package/dist/agent/tools/read_notion_comments.js +21 -2
  48. package/dist/agent/tools/read_notion_comments.js.map +1 -1
  49. package/dist/agent/tools/read_notion_page.d.ts.map +1 -1
  50. package/dist/agent/tools/read_notion_page.js +2 -0
  51. package/dist/agent/tools/read_notion_page.js.map +1 -1
  52. 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
- export const NOTION_API_BASE = "https://api.notion.com";
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
- export async function retrieveBlock(blockId: string, opts: NotionCallOpts): Promise<NotionBlockParent> {
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
- export const BLOCK_PARENT_MAX_HOPS = 5;
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
- export async function retrievePage(pageId: string, opts: NotionCallOpts): Promise<NotionPageParent> {
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): Record<string, string> {
367
- const out: Record<string, string> = {};
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, exported so the three
662
- * behaviours are testable without a store or a live wake.
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
- export function notionExternalAuthorDecision(d: {
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
- export interface NotionWebhookTimingDetail {
21
- readonly [key: string]: unknown;
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;
@@ -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 instanceof Error ? err.message : String(err)).slice(0, 300);
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: {