@zackbart/connecta 0.34.1 → 0.36.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/CHANGELOG.md +418 -0
- package/README.md +65 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2044 -1161
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +751 -281
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +29 -7
- package/dist/connectors/api-connector.js +145 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +227 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +379 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/contract.d.ts +8 -3
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +427 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/kit.js +146 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +801 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1023 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +733 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +65 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +6 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +552 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -223
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +9 -109
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +28 -7
- package/documentation/architecture.md +2409 -255
- package/documentation/auth.md +867 -114
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +84 -48
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +1347 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +30 -50
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/operator-ui/agents.d.ts +0 -74
- package/dist/operator-ui/agents.js +0 -104
- package/dist/operator-ui/config-summary.js +0 -565
- package/dist/operator-ui/connection-map.js +0 -376
- package/dist/operator-ui/endpoints.js +0 -221
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -87
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -123
- package/dist/operator-ui/setup-commands.js +0 -51
- package/dist/operator-ui/view.d.ts +0 -401
- package/dist/operator-ui/view.js +0 -705
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/dispatch.js +0 -41
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/tools.js +0 -569
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -1,1226 +0,0 @@
|
|
|
1
|
-
import { retryAfterMs } from "../../connectors/guarded-fetch.js";
|
|
2
|
-
import { ConnectorCallError } from "../../errors.js";
|
|
3
|
-
import { OperationIndex } from "../_shared/rest/operation-index.js";
|
|
4
|
-
import { callRest, restCall, restTools, restTransport, } from "../_shared/rest/tools.js";
|
|
5
|
-
import { carriesWithheldLink, valueSafety } from "../_shared/rest/value-safety.js";
|
|
6
|
-
import { openapi } from "./openapi.generated.js";
|
|
7
|
-
import { NOTION_FILE_HOSTS, NOTION_VALUE_SAFETY, sanitizeLinks } from "./value-safety.js";
|
|
8
|
-
/** Notion's REST origin. Every request this connector sends goes to exactly this host. */
|
|
9
|
-
export const NOTION_API_BASE_URL = "https://api.notion.com";
|
|
10
|
-
/**
|
|
11
|
-
* Pinned with no override, and equal to the pinned index's `Notion-Version`
|
|
12
|
-
* (a test holds them together, so regenerating the index against a newer
|
|
13
|
-
* version fails until this moves with it). Notion's date-named versions keep
|
|
14
|
-
* working indefinitely, which makes an override look harmless; it is not.
|
|
15
|
-
* `2026-03-11` is where databases split into data sources, `archived` became
|
|
16
|
-
* `in_trash`, and block append took a `position` object instead of an `after`
|
|
17
|
-
* string. Every projection, write body, and index contract here assumes those
|
|
18
|
-
* shapes, so an older version would return quietly wrong results rather than
|
|
19
|
-
* fail loudly.
|
|
20
|
-
*/
|
|
21
|
-
export const NOTION_API_VERSION = "2026-03-11";
|
|
22
|
-
/** Notion's hard cap on `page_size` for every paginated endpoint. */
|
|
23
|
-
export const MAX_PAGE_SIZE = 100;
|
|
24
|
-
/** Page retrieval caps `filter_properties` at 100; data-source queries declare no cap. */
|
|
25
|
-
const MAX_PAGE_PROPERTY_FILTERS = 100;
|
|
26
|
-
/** Notion's cap on `children` per append, and on blocks per page create. */
|
|
27
|
-
const MAX_CHILDREN_PER_REQUEST = 100;
|
|
28
|
-
/** Notion's cap on one rich-text run's `text.content`, and on runs in one rich-text array. */
|
|
29
|
-
const MAX_TEXT_CONTENT = 2_000;
|
|
30
|
-
const MAX_RICH_TEXT_RUNS = 100;
|
|
31
|
-
/**
|
|
32
|
-
* Ceiling on the fetches one `integration_get_page_content` walk may spend. Call admission
|
|
33
|
-
* meters tool calls, not the requests inside them, so a deep `depth` would
|
|
34
|
-
* otherwise drain the budget invisibly; the walk stops here and reports
|
|
35
|
-
* `truncated: true` instead.
|
|
36
|
-
*/
|
|
37
|
-
const MAX_CONTENT_REQUESTS = 20;
|
|
38
|
-
/**
|
|
39
|
-
* The largest response this connection will read. `page_size` tops out at
|
|
40
|
-
* 100 and every payload is JSON, so four mebibytes is a ceiling on absurdity,
|
|
41
|
-
* not a budget any real read has to think about.
|
|
42
|
-
*/
|
|
43
|
-
const NOTION_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
44
|
-
const READ_CAP_MS = 60_000;
|
|
45
|
-
/**
|
|
46
|
-
* POSTs that only read, admitted by `notion_api_read`. Each is a query over
|
|
47
|
-
* existing objects that persists nothing. `POST /v1/views/{view_id}/queries`
|
|
48
|
-
* is deliberately absent: it creates a stored view query that lives until
|
|
49
|
-
* `DELETE /v1/views/{view_id}/queries/{query_id}` removes it.
|
|
50
|
-
*/
|
|
51
|
-
const READ_POSTS = [
|
|
52
|
-
["POST", "/v1/search", "Searches titles shared with the integration; Notion persists nothing."],
|
|
53
|
-
[
|
|
54
|
-
"POST",
|
|
55
|
-
"/v1/data_sources/{data_source_id}/query",
|
|
56
|
-
"Lists a data source's rows by filter and sort; persists nothing.",
|
|
57
|
-
],
|
|
58
|
-
["POST", "/v1/blocks/meeting_notes/query", "Lists meeting notes by filter and sort; persists nothing."],
|
|
59
|
-
["POST", "/v1/agents/query", "Lists custom agents by name, filter, and sort; persists nothing."],
|
|
60
|
-
["POST", "/v1/sessions/query", "Lists agent sessions by title, filter, and sort; persists nothing."],
|
|
61
|
-
["POST", "/v1/sessions/{session_id}/events/query", "Lists one session's events; persists nothing."],
|
|
62
|
-
];
|
|
63
|
-
function isRecord(value) {
|
|
64
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
|
-
}
|
|
66
|
-
function invalid(message) {
|
|
67
|
-
throw new ConnectorCallError("invalid_args", message);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Notion repeats `Retry-After` in the body as `additional_data.retry_after`,
|
|
71
|
-
* a string of whole seconds, so a proxy that drops the header still leaves a
|
|
72
|
-
* window to wait out.
|
|
73
|
-
*/
|
|
74
|
-
function bodyRetryAfterMs(additional) {
|
|
75
|
-
const raw = isRecord(additional) ? additional["retry_after"] : undefined;
|
|
76
|
-
const seconds = typeof raw === "string" || typeof raw === "number" ? Number(raw) : Number.NaN;
|
|
77
|
-
return Number.isFinite(seconds) && seconds >= 0 ? Math.trunc(seconds * 1000) : undefined;
|
|
78
|
-
}
|
|
79
|
-
/** A Notion object id, dashed or not: the only committed-id shape shown when error text is withheld. */
|
|
80
|
-
const NOTION_ID = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
|
|
81
|
-
/**
|
|
82
|
-
* The ids a 503 reports as already committed, so a caller reads them instead
|
|
83
|
-
* of repeating the write. For an operation whose error text is withheld, only
|
|
84
|
-
* ids shaped as Notion ids are named, since anything else could echo what the
|
|
85
|
-
* request sent; the write is still reported as committed.
|
|
86
|
-
*/
|
|
87
|
-
function committedIds(additional, withheld) {
|
|
88
|
-
if (!isRecord(additional))
|
|
89
|
-
return "";
|
|
90
|
-
const resource = additional["committed_resource_id"];
|
|
91
|
-
const children = Array.isArray(additional["committed_child_ids"])
|
|
92
|
-
? additional["committed_child_ids"].filter((child) => typeof child === "string")
|
|
93
|
-
: [];
|
|
94
|
-
if (typeof resource !== "string" || resource === "")
|
|
95
|
-
return "";
|
|
96
|
-
if (withheld && ![resource, ...children].every((committed) => NOTION_ID.test(committed))) {
|
|
97
|
-
return " Notion committed the write before failing; what it committed is withheld because this operation handles credentials. Read the object instead of repeating the write.";
|
|
98
|
-
}
|
|
99
|
-
return children.length
|
|
100
|
-
? ` Notion committed the write before failing: it appended ${children.length} block(s) (${children.join(", ")}) under ${resource}. Read that block instead of repeating the write.`
|
|
101
|
-
: ` Notion committed the write before failing: the object it created or changed is ${resource}. Read it instead of repeating the write.`;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Notion's own next step for a failed write, `additional_data.retry_guidance`.
|
|
105
|
-
* An update to an existing object that was saved anyway names no committed
|
|
106
|
-
* id, only this, so it is relayed as Notion wrote it.
|
|
107
|
-
*/
|
|
108
|
-
function retryGuidance(additional) {
|
|
109
|
-
const guidance = isRecord(additional) ? additional["retry_guidance"] : undefined;
|
|
110
|
-
const lines = (Array.isArray(guidance) ? guidance : [guidance]).filter((line) => typeof line === "string" && line.trim() !== "");
|
|
111
|
-
return lines.map((line) => line.trim()).join(" ");
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Map to what the caller should do next, not to what Notion's `code` says
|
|
115
|
-
* happened. The two that are easy to mistranslate:
|
|
116
|
-
*
|
|
117
|
-
* - **403 is not an authentication failure.** The token is fine; the integration
|
|
118
|
-
* lacks a capability, was never shared the object, or the workspace is past
|
|
119
|
-
* its block limit, and neither `authorize_connector` nor a new token can fix
|
|
120
|
-
* it. Notion's message says which. Non-retryable call failure.
|
|
121
|
-
* - **404 does not prove absence.** Notion returns `object_not_found` both for an
|
|
122
|
-
* object that does not exist and for one never shared with the integration, and
|
|
123
|
-
* will not say which — so not `not_found`, which exists to assert absence
|
|
124
|
-
* (H11). The message names both possibilities.
|
|
125
|
-
*
|
|
126
|
-
* A 503 on a write may still have been saved. When Notion names what it
|
|
127
|
-
* committed, or gives its own retry guidance, the failure says so and stops
|
|
128
|
-
* being retryable; otherwise the shared path reports a write's outcome as
|
|
129
|
-
* unknown (INV-9).
|
|
130
|
-
*
|
|
131
|
-
* `withheld` replaces every free-text field (message, retry guidance,
|
|
132
|
-
* committed ids) for an operation whose error text the review withholds.
|
|
133
|
-
* Text that does pass has had its pre-signed file links withheld first
|
|
134
|
-
* (`sanitizeLinks`, as on success), since a message can echo the Markdown it
|
|
135
|
-
* rejected.
|
|
136
|
-
*/
|
|
137
|
-
function notionFailure(status, headers, payload, withheld = false) {
|
|
138
|
-
const body = isRecord(payload) ? payload : undefined;
|
|
139
|
-
const code = typeof body?.["code"] === "string" ? body["code"] : undefined;
|
|
140
|
-
const additional = body?.["additional_data"];
|
|
141
|
-
// For an operation the value-safety review withholds, Notion's message is
|
|
142
|
-
// replaced; its code still routes.
|
|
143
|
-
const detail = withheld
|
|
144
|
-
? `Notion answered HTTP ${status}; its message is withheld because this operation handles credentials.`
|
|
145
|
-
: typeof body?.["message"] === "string" && body["message"].trim()
|
|
146
|
-
? body["message"].trim()
|
|
147
|
-
: `Notion returned HTTP ${status}.`;
|
|
148
|
-
const retryAfter = retryAfterMs(headers) ?? bodyRetryAfterMs(additional);
|
|
149
|
-
const labelled = code ? `Notion ${code}: ${detail}` : detail;
|
|
150
|
-
if (status === 429) {
|
|
151
|
-
const reason = isRecord(additional) ? additional["rate_limit_reason"] : undefined;
|
|
152
|
-
if (reason === "public_api_request_blocked") {
|
|
153
|
-
return new ConnectorCallError("connector_call_failed", `${labelled} (limit: ${reason}) Notion has restricted this integration's API access. Retrying will not help; an operator must contact Notion support.`, { retryable: false });
|
|
154
|
-
}
|
|
155
|
-
const scope = reason === "public_api_space_request_rate_limit"
|
|
156
|
-
? " The workspace's shared budget across all its connections is spent, and its window can exceed a minute."
|
|
157
|
-
: " Notion allows each integration 180 requests a minute (600 on Business and Enterprise), and a separate budget shared by the whole workspace.";
|
|
158
|
-
return new ConnectorCallError("rate_limited", `${labelled}${typeof reason === "string" ? ` (limit: ${reason})` : ""}${scope} Wait out the retry window.`, { retryAfterMs: retryAfter ?? 1_000 });
|
|
159
|
-
}
|
|
160
|
-
if (status === 529) {
|
|
161
|
-
// Notion documents 529 alongside 429: back off and respect Retry-After.
|
|
162
|
-
return new ConnectorCallError("unavailable", `${labelled} Notion is overloaded; retry after the reported window.`, {
|
|
163
|
-
retryAfterMs: retryAfter ?? 5_000,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
if (status === 401) {
|
|
167
|
-
return new ConnectorCallError("auth_required", `${labelled} The Notion integration token is missing or invalid. Call authorize_connector for recovery options. When available, an operator can set a valid token in this connection in the operator UI.`);
|
|
168
|
-
}
|
|
169
|
-
if (status === 403) {
|
|
170
|
-
return new ConnectorCallError("provider_permission_denied", `${labelled} The token is valid but Notion refused this operation, and Notion's message says why. Usually an operator must enable the matching capability on the Notion integration (comment capabilities are off by default) or share the object with it; a workspace past its plan's block limit also answers 403. Re-authorizing will not help.`, { retryable: false });
|
|
171
|
-
}
|
|
172
|
-
if (status === 404) {
|
|
173
|
-
return new ConnectorCallError("connector_call_failed", `${labelled} Notion returns this both for an object that does not exist and for one that exists but has not been shared with this integration — do not treat it as proof of deletion. Confirm the id, then confirm the page or database is shared with the integration in Notion.`, { retryable: false });
|
|
174
|
-
}
|
|
175
|
-
if (status === 400) {
|
|
176
|
-
// Every documented 400 (validation_error, invalid_json, invalid_request,
|
|
177
|
-
// invalid_request_url, missing_version, invalid_beta) is a malformed
|
|
178
|
-
// request, which is exactly what invalid_args means to the caller.
|
|
179
|
-
return new ConnectorCallError("invalid_args", labelled);
|
|
180
|
-
}
|
|
181
|
-
if (status === 409) {
|
|
182
|
-
return new ConnectorCallError("unavailable", `${labelled} Notion reported a write conflict; this is safe to retry.`, {
|
|
183
|
-
retryAfterMs: retryAfter ?? 1_000,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
if (status >= 500) {
|
|
187
|
-
const committed = committedIds(additional, withheld);
|
|
188
|
-
if (committed) {
|
|
189
|
-
return new ConnectorCallError("connector_call_failed", `${labelled}${committed}`, { retryable: false });
|
|
190
|
-
}
|
|
191
|
-
const guidance = retryGuidance(additional);
|
|
192
|
-
if (guidance) {
|
|
193
|
-
return new ConnectorCallError("connector_call_failed", withheld
|
|
194
|
-
? `${labelled} Notion sent retry guidance, withheld because this operation handles credentials. Read the object before repeating the write.`
|
|
195
|
-
: `${labelled} Notion's guidance: ${guidance}`, { retryable: false });
|
|
196
|
-
}
|
|
197
|
-
return new ConnectorCallError("unavailable", `${labelled} Notion is failing upstream.`, retryAfter !== undefined ? { retryAfterMs: retryAfter } : {});
|
|
198
|
-
}
|
|
199
|
-
return new ConnectorCallError("connector_call_failed", labelled, { retryable: false });
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Notion lists carry `results` and `has_more`; the cursor, when there is one,
|
|
203
|
-
* is `next_cursor` and goes back as `start_cursor`: a query parameter on GET
|
|
204
|
-
* lists, a body field on the read-only POST queries. Two shapes differ:
|
|
205
|
-
*
|
|
206
|
-
* - `POST /v1/views/{view_id}/queries` answers a `view_query` object whose
|
|
207
|
-
* first page continues at `GET /v1/views/{view_id}/queries/{query_id}`
|
|
208
|
-
* (until `expires_at`), so the page names that path.
|
|
209
|
-
* - `POST /v1/blocks/meeting_notes/query` has no cursor at all: `has_more`
|
|
210
|
-
* past its `limit` (50 at most) can only be answered by a narrower filter.
|
|
211
|
-
* - A data source or view query stops at 10,000 results with `has_more: false`
|
|
212
|
-
* and `request_status.type: "incomplete"`. That is a cut-off with no cursor,
|
|
213
|
-
* reported like the meeting-note one: `hasMore` without `next`.
|
|
214
|
-
*
|
|
215
|
-
* Every shape keeps `hasMore`, so a `select` that drops the body's own
|
|
216
|
-
* completeness fields cannot hide that a list was cut short.
|
|
217
|
-
*/
|
|
218
|
-
function page(data, call) {
|
|
219
|
-
if (!isRecord(data) || typeof data["has_more"] !== "boolean" || !Array.isArray(data["results"]))
|
|
220
|
-
return undefined;
|
|
221
|
-
if (!data["has_more"] && incomplete(data))
|
|
222
|
-
return { hasMore: true };
|
|
223
|
-
const hasMore = data["has_more"];
|
|
224
|
-
const next = typeof data["next_cursor"] === "string" && data["next_cursor"] !== "" ? data["next_cursor"] : undefined;
|
|
225
|
-
if (!hasMore || !next)
|
|
226
|
-
return { hasMore };
|
|
227
|
-
if (data["object"] === "view_query" && typeof data["id"] === "string" && typeof data["view_id"] === "string") {
|
|
228
|
-
return {
|
|
229
|
-
hasMore,
|
|
230
|
-
next,
|
|
231
|
-
param: "start_cursor",
|
|
232
|
-
in: "query",
|
|
233
|
-
path: `/v1/views/${encodeURIComponent(data["view_id"])}/queries/${encodeURIComponent(data["id"])}`,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
return { hasMore, next, param: "start_cursor", ...(call.method === "GET" ? {} : { in: "body" }) };
|
|
237
|
-
}
|
|
238
|
-
/** Whether a query stopped at Notion's per-query result cap rather than at its last row. */
|
|
239
|
-
function incomplete(data) {
|
|
240
|
-
const status = data["request_status"];
|
|
241
|
-
return isRecord(status) && status["type"] === "incomplete";
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* A read copied into a write: a file link whose signature this connector
|
|
245
|
-
* withheld would be stored as the placeholder, or fail an exact `old_str`
|
|
246
|
-
* match. Refused before dispatch, named tools and generic writes alike.
|
|
247
|
-
*/
|
|
248
|
-
function refuseWithheldLinks(call) {
|
|
249
|
-
if (call.method === "GET" || call.method === "HEAD")
|
|
250
|
-
return;
|
|
251
|
-
if (READ_POSTS.some(([method, path]) => method === call.method && path === call.op.path))
|
|
252
|
-
return;
|
|
253
|
-
if (!carriesWithheldLink(call.body, NOTION_FILE_HOSTS))
|
|
254
|
-
return;
|
|
255
|
-
invalid("This write carries a Notion file link whose signature Connecta withheld ([redacted]), so Notion would store the placeholder. A redacted file URL cannot be copied into a write: re-upload the file (POST /v1/file_uploads), reference the existing block instead, and match Markdown edits on the text around the media rather than its URL.");
|
|
256
|
-
}
|
|
257
|
-
/** The pinned index, shared by every Notion REST connector in the deployment. */
|
|
258
|
-
let index;
|
|
259
|
-
let transport;
|
|
260
|
-
function notionIndex() {
|
|
261
|
-
index ??= new OperationIndex(openapi, { vendor: "notion", title: "Notion" });
|
|
262
|
-
return index;
|
|
263
|
-
}
|
|
264
|
-
/** The reviewed value-safety table over the pinned index. */
|
|
265
|
-
const SAFETY = valueSafety(NOTION_VALUE_SAFETY, () => notionIndex());
|
|
266
|
-
function notionTransport() {
|
|
267
|
-
transport ??= restTransport({
|
|
268
|
-
provider: "Notion",
|
|
269
|
-
baseUrl: NOTION_API_BASE_URL,
|
|
270
|
-
headers: { "Notion-Version": NOTION_API_VERSION },
|
|
271
|
-
maxResponseBytes: NOTION_MAX_RESPONSE_BYTES,
|
|
272
|
-
timeoutMs: READ_CAP_MS,
|
|
273
|
-
authenticate: async (ctx) => {
|
|
274
|
-
const token = (await ctx.credential?.get())?.trim();
|
|
275
|
-
if (!token) {
|
|
276
|
-
throw new ConnectorCallError("auth_required", "No Notion integration token is configured for this connector. Call authorize_connector for recovery options. When available, an operator can add the token in this connection in the operator UI.");
|
|
277
|
-
}
|
|
278
|
-
return { Authorization: `Bearer ${token}` };
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
return transport;
|
|
282
|
-
}
|
|
283
|
-
const VENDOR = {
|
|
284
|
-
vendor: "notion",
|
|
285
|
-
title: "Notion",
|
|
286
|
-
get index() {
|
|
287
|
-
return notionIndex();
|
|
288
|
-
},
|
|
289
|
-
transport(server) {
|
|
290
|
-
return server === undefined
|
|
291
|
-
? notionTransport()
|
|
292
|
-
: `This operation is served from ${server}, which this connector does not reach.`;
|
|
293
|
-
},
|
|
294
|
-
failure: (status, headers, payload, call) => notionFailure(status, headers, sanitizeLinks(payload), call ? SAFETY.withholdsErrors(call.op) : false),
|
|
295
|
-
readPosts: READ_POSTS,
|
|
296
|
-
valueSafety: SAFETY,
|
|
297
|
-
// Not an envelope to unwrap: page Markdown carries pre-signed file links
|
|
298
|
-
// inside its text, where redaction's URL rule cannot reach. Every success
|
|
299
|
-
// response passes here, named tools and paging included.
|
|
300
|
-
result: sanitizeLinks,
|
|
301
|
-
admit: refuseWithheldLinks,
|
|
302
|
-
page,
|
|
303
|
-
upload: 'Create the upload with POST /v1/file_uploads and mode "external_url" instead; this connector sends no multipart bodies.',
|
|
304
|
-
bodyHint: "Notion's JSON request body as notion_api_details lists it",
|
|
305
|
-
};
|
|
306
|
-
/**
|
|
307
|
-
* Send one named-tool request on the shared path (same transport, framing,
|
|
308
|
-
* failure mapping) and insist on an object back. A sent write whose answer
|
|
309
|
-
* cannot be read stays unknown: the caller is told not to repeat it.
|
|
310
|
-
*/
|
|
311
|
-
async function notion(ctx, method, path, input = {}) {
|
|
312
|
-
const { data } = await callRest(VENDOR, restCall(VENDOR, method, path, input), ctx);
|
|
313
|
-
if (!isRecord(data)) {
|
|
314
|
-
throw new ConnectorCallError("connector_call_failed", "Notion returned an unreadable or non-object response for a successful status. The request was sent, but its result could not be read; do not repeat a write to recover its result.", { retryable: false });
|
|
315
|
-
}
|
|
316
|
-
return data;
|
|
317
|
-
}
|
|
318
|
-
const id = (value) => encodeURIComponent(String(value));
|
|
319
|
-
// Projections. Notion wraps every property in a discriminated object, every
|
|
320
|
-
// string in an array of annotated rich-text runs, and every user in a nested
|
|
321
|
-
// object, so a small query is tens of kilobytes of structure around a few
|
|
322
|
-
// hundred bytes of meaning. Every named read collapses that to ids, plain
|
|
323
|
-
// text, and flattened values. Nothing is lost for good: the reads that can
|
|
324
|
-
// drop something take `raw: true` and return Notion's untouched response.
|
|
325
|
-
//
|
|
326
|
-
// Unknown types unwrap rather than switch exhaustively, because Notion ships
|
|
327
|
-
// additive changes to every pinned version at once — a property type newer than
|
|
328
|
-
// this release degrades to its raw value instead of vanishing.
|
|
329
|
-
/** Concatenate a rich-text array to its plain text. Safe for every variant. */
|
|
330
|
-
function plainText(value) {
|
|
331
|
-
if (!Array.isArray(value))
|
|
332
|
-
return "";
|
|
333
|
-
return value.map((run) => (typeof run?.plain_text === "string" ? run.plain_text : "")).join("");
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Wrap a plain string as the rich-text array Notion expects. Notion rejects a
|
|
337
|
-
* run over 2,000 characters, so a longer string becomes consecutive runs, cut
|
|
338
|
-
* between UTF-16 surrogate pairs, never inside one. Past 100 runs Notion
|
|
339
|
-
* rejects the array itself, so that refuses here with the way forward.
|
|
340
|
-
*/
|
|
341
|
-
function richText(value, field) {
|
|
342
|
-
const runs = [];
|
|
343
|
-
let rest = value;
|
|
344
|
-
while (rest.length > MAX_TEXT_CONTENT) {
|
|
345
|
-
const high = rest.charCodeAt(MAX_TEXT_CONTENT - 1);
|
|
346
|
-
const cut = high >= 0xd800 && high <= 0xdbff ? MAX_TEXT_CONTENT - 1 : MAX_TEXT_CONTENT;
|
|
347
|
-
runs.push(rest.slice(0, cut));
|
|
348
|
-
rest = rest.slice(cut);
|
|
349
|
-
}
|
|
350
|
-
runs.push(rest);
|
|
351
|
-
if (runs.length > MAX_RICH_TEXT_RUNS) {
|
|
352
|
-
invalid(`${field} is ${value.length} characters; Notion accepts at most ${MAX_RICH_TEXT_RUNS} runs of ${MAX_TEXT_CONTENT} characters in one rich-text value. Split it across several blocks.`);
|
|
353
|
-
}
|
|
354
|
-
return runs.map((content) => ({ type: "text", text: { content } }));
|
|
355
|
-
}
|
|
356
|
-
function userRef(value) {
|
|
357
|
-
if (!value || typeof value !== "object")
|
|
358
|
-
return null;
|
|
359
|
-
return {
|
|
360
|
-
id: typeof value.id === "string" ? value.id : null,
|
|
361
|
-
...(typeof value.name === "string" ? { name: value.name } : {}),
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Flatten one Notion property value.
|
|
366
|
-
*
|
|
367
|
-
* The `default` branch is not laziness: Notion adds property types to every
|
|
368
|
-
* API version simultaneously, so an exhaustive switch would start returning
|
|
369
|
-
* `undefined` for a type that shipped after this release. Unwrapping
|
|
370
|
-
* `value[value.type]` degrades an unknown type to its raw payload instead.
|
|
371
|
-
*/
|
|
372
|
-
function projectPropertyValue(value) {
|
|
373
|
-
const type = value?.type;
|
|
374
|
-
switch (type) {
|
|
375
|
-
case "title":
|
|
376
|
-
case "rich_text":
|
|
377
|
-
return plainText(value[type]);
|
|
378
|
-
case "number":
|
|
379
|
-
case "checkbox":
|
|
380
|
-
case "url":
|
|
381
|
-
case "email":
|
|
382
|
-
case "phone_number":
|
|
383
|
-
case "created_time":
|
|
384
|
-
case "last_edited_time":
|
|
385
|
-
return value[type] ?? null;
|
|
386
|
-
case "select":
|
|
387
|
-
case "status":
|
|
388
|
-
return value[type]?.name ?? null;
|
|
389
|
-
case "multi_select":
|
|
390
|
-
return (value.multi_select ?? []).map((option) => option?.name ?? null);
|
|
391
|
-
case "date":
|
|
392
|
-
return value.date
|
|
393
|
-
? {
|
|
394
|
-
start: value.date.start ?? null,
|
|
395
|
-
end: value.date.end ?? null,
|
|
396
|
-
...(value.date.time_zone ? { time_zone: value.date.time_zone } : {}),
|
|
397
|
-
}
|
|
398
|
-
: null;
|
|
399
|
-
case "people":
|
|
400
|
-
return (value.people ?? []).map(userRef);
|
|
401
|
-
case "created_by":
|
|
402
|
-
case "last_edited_by":
|
|
403
|
-
return userRef(value[type]);
|
|
404
|
-
case "files":
|
|
405
|
-
return (value.files ?? []).map((file) => ({
|
|
406
|
-
name: file?.name ?? null,
|
|
407
|
-
// A `file` upload carries a signed URL that expires; an `external` one
|
|
408
|
-
// is a plain link. Agents want the link either way.
|
|
409
|
-
url: file?.external?.url ?? file?.file?.url ?? null,
|
|
410
|
-
}));
|
|
411
|
-
case "relation":
|
|
412
|
-
return (value.relation ?? [])
|
|
413
|
-
.map((related) => related?.id ?? null)
|
|
414
|
-
.filter((related) => typeof related === "string");
|
|
415
|
-
case "formula":
|
|
416
|
-
// Notion answers `unsupported` for a value it cannot compute through the API.
|
|
417
|
-
return value.formula?.type === "unsupported" ? null : (value.formula?.[value.formula?.type] ?? null);
|
|
418
|
-
case "rollup": {
|
|
419
|
-
const rollup = value.rollup;
|
|
420
|
-
if (!rollup || rollup.type === "unsupported")
|
|
421
|
-
return null;
|
|
422
|
-
if (rollup.type === "array") {
|
|
423
|
-
return (rollup.array ?? []).map(projectPropertyValue);
|
|
424
|
-
}
|
|
425
|
-
return rollup[rollup.type] ?? null;
|
|
426
|
-
}
|
|
427
|
-
case "unique_id":
|
|
428
|
-
return value.unique_id?.prefix
|
|
429
|
-
? `${value.unique_id.prefix}-${value.unique_id.number}`
|
|
430
|
-
: (value.unique_id?.number ?? null);
|
|
431
|
-
case "verification":
|
|
432
|
-
return value.verification?.state ?? null;
|
|
433
|
-
default:
|
|
434
|
-
return type ? (value[type] ?? null) : null;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
function projectProperties(source, select) {
|
|
438
|
-
const properties = {};
|
|
439
|
-
const truncated = [];
|
|
440
|
-
if (!source || typeof source !== "object")
|
|
441
|
-
return { properties, truncated };
|
|
442
|
-
for (const [name, value] of Object.entries(source)) {
|
|
443
|
-
if (select && !select.includes(name))
|
|
444
|
-
continue;
|
|
445
|
-
properties[name] = projectPropertyValue(value);
|
|
446
|
-
if (value?.has_more === true) {
|
|
447
|
-
truncated.push({ name, id: typeof value?.id === "string" ? value.id : null });
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return { properties, truncated };
|
|
451
|
-
}
|
|
452
|
-
/** The title property's name is arbitrary; its `type` is not. */
|
|
453
|
-
function pageTitle(source) {
|
|
454
|
-
if (!source || typeof source !== "object")
|
|
455
|
-
return "";
|
|
456
|
-
for (const value of Object.values(source)) {
|
|
457
|
-
if (value?.type === "title")
|
|
458
|
-
return plainText(value.title);
|
|
459
|
-
}
|
|
460
|
-
return "";
|
|
461
|
-
}
|
|
462
|
-
function parentRef(parent) {
|
|
463
|
-
if (!parent || typeof parent !== "object")
|
|
464
|
-
return null;
|
|
465
|
-
const type = parent.type;
|
|
466
|
-
if (typeof type !== "string")
|
|
467
|
-
return null;
|
|
468
|
-
return { type, id: typeof parent[type] === "string" ? parent[type] : null };
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* An icon as one string: the emoji itself, a native Notion icon's name, a
|
|
472
|
-
* workspace custom emoji as `:name:`, or an image URL.
|
|
473
|
-
*/
|
|
474
|
-
function iconRef(icon) {
|
|
475
|
-
if (!icon || typeof icon !== "object")
|
|
476
|
-
return null;
|
|
477
|
-
if (typeof icon.emoji === "string")
|
|
478
|
-
return icon.emoji;
|
|
479
|
-
if (icon.type === "icon" && typeof icon.icon?.name === "string")
|
|
480
|
-
return icon.icon.name;
|
|
481
|
-
if (icon.type === "custom_emoji" && typeof icon.custom_emoji?.name === "string") {
|
|
482
|
-
return `:${icon.custom_emoji.name}:`;
|
|
483
|
-
}
|
|
484
|
-
return icon.external?.url ?? icon.file?.url ?? null;
|
|
485
|
-
}
|
|
486
|
-
function projectPage(page, select) {
|
|
487
|
-
const { properties, truncated } = projectProperties(page?.properties, select);
|
|
488
|
-
return {
|
|
489
|
-
id: page?.id ?? null,
|
|
490
|
-
object: "page",
|
|
491
|
-
title: pageTitle(page?.properties),
|
|
492
|
-
url: page?.url ?? null,
|
|
493
|
-
parent: parentRef(page?.parent),
|
|
494
|
-
icon: iconRef(page?.icon),
|
|
495
|
-
created_time: page?.created_time ?? null,
|
|
496
|
-
last_edited_time: page?.last_edited_time ?? null,
|
|
497
|
-
created_by: userRef(page?.created_by),
|
|
498
|
-
last_edited_by: userRef(page?.last_edited_by),
|
|
499
|
-
in_trash: page?.in_trash === true,
|
|
500
|
-
is_archived: page?.is_archived === true,
|
|
501
|
-
properties,
|
|
502
|
-
...(truncated.length ? { truncated_properties: truncated } : {}),
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Identity fields only, no properties. A 25-result search across a populated
|
|
507
|
-
* database would otherwise drag back hundreds of flattened values for results
|
|
508
|
-
* the agent is about to discard; `integration_get_page` fetches them for the one that hit.
|
|
509
|
-
*/
|
|
510
|
-
function projectSearchHit(hit) {
|
|
511
|
-
if (hit?.object === "data_source") {
|
|
512
|
-
return {
|
|
513
|
-
id: hit?.id ?? null,
|
|
514
|
-
object: "data_source",
|
|
515
|
-
title: plainText(hit?.title) || (hit?.name ?? ""),
|
|
516
|
-
database_id: hit?.parent?.database_id ?? null,
|
|
517
|
-
url: hit?.url ?? null,
|
|
518
|
-
last_edited_time: hit?.last_edited_time ?? null,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
return {
|
|
522
|
-
id: hit?.id ?? null,
|
|
523
|
-
object: hit?.object ?? "page",
|
|
524
|
-
title: hit?.object === "page" ? pageTitle(hit?.properties) : plainText(hit?.title),
|
|
525
|
-
url: hit?.url ?? null,
|
|
526
|
-
parent: parentRef(hit?.parent),
|
|
527
|
-
last_edited_time: hit?.last_edited_time ?? null,
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
/** Flatten one block to its text plus the few fields its type actually adds. */
|
|
531
|
-
function projectBlock(block, depth) {
|
|
532
|
-
const type = block?.type;
|
|
533
|
-
const payload = type ? block?.[type] : undefined;
|
|
534
|
-
const projected = {
|
|
535
|
-
id: block?.id ?? null,
|
|
536
|
-
type: type ?? "unsupported",
|
|
537
|
-
depth,
|
|
538
|
-
text: plainText(payload?.rich_text),
|
|
539
|
-
has_children: block?.has_children === true,
|
|
540
|
-
};
|
|
541
|
-
switch (type) {
|
|
542
|
-
case "to_do":
|
|
543
|
-
projected["checked"] = payload?.checked === true;
|
|
544
|
-
break;
|
|
545
|
-
case "code":
|
|
546
|
-
projected["language"] = payload?.language ?? null;
|
|
547
|
-
break;
|
|
548
|
-
case "child_page":
|
|
549
|
-
case "child_database":
|
|
550
|
-
// Notion gives these a plain string title, not a rich-text array.
|
|
551
|
-
projected["text"] = typeof payload?.title === "string" ? payload.title : "";
|
|
552
|
-
break;
|
|
553
|
-
case "image":
|
|
554
|
-
case "video":
|
|
555
|
-
case "file":
|
|
556
|
-
case "pdf":
|
|
557
|
-
projected["url"] = payload?.external?.url ?? payload?.file?.url ?? null;
|
|
558
|
-
projected["text"] = plainText(payload?.caption);
|
|
559
|
-
break;
|
|
560
|
-
case "bookmark":
|
|
561
|
-
case "embed":
|
|
562
|
-
case "link_preview":
|
|
563
|
-
projected["url"] = payload?.url ?? null;
|
|
564
|
-
projected["text"] = plainText(payload?.caption);
|
|
565
|
-
break;
|
|
566
|
-
case "equation":
|
|
567
|
-
projected["text"] = payload?.expression ?? "";
|
|
568
|
-
break;
|
|
569
|
-
case "table_row":
|
|
570
|
-
projected["cells"] = (payload?.cells ?? []).map(plainText);
|
|
571
|
-
break;
|
|
572
|
-
case "callout":
|
|
573
|
-
projected["icon"] = iconRef(payload?.icon);
|
|
574
|
-
break;
|
|
575
|
-
default:
|
|
576
|
-
// A block type this projection does not model keeps its payload verbatim
|
|
577
|
-
// rather than collapsing to an empty string, so a block type newer than
|
|
578
|
-
// this release still carries its content.
|
|
579
|
-
if (carriesUnprojectedContent(payload))
|
|
580
|
-
projected["raw"] = payload;
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
583
|
-
return projected;
|
|
584
|
-
}
|
|
585
|
-
function carriesUnprojectedContent(payload) {
|
|
586
|
-
if (!payload || typeof payload !== "object")
|
|
587
|
-
return false;
|
|
588
|
-
if (Array.isArray(payload.rich_text))
|
|
589
|
-
return false;
|
|
590
|
-
return Object.keys(payload).some((key) => key !== "color");
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* A data source's schema, reduced to what a caller needs to filter and write.
|
|
594
|
-
*
|
|
595
|
-
* Select and status options are kept because a filter or a write that invents
|
|
596
|
-
* an option name fails; everything else about a property collapses to its type.
|
|
597
|
-
*/
|
|
598
|
-
function projectSchemaProperty(property) {
|
|
599
|
-
const type = property?.type;
|
|
600
|
-
const projected = { id: property?.id ?? null, type: type ?? null };
|
|
601
|
-
const payload = type ? property?.[type] : undefined;
|
|
602
|
-
if (type === "select" || type === "multi_select") {
|
|
603
|
-
projected["options"] = (payload?.options ?? []).map((option) => option?.name ?? null);
|
|
604
|
-
}
|
|
605
|
-
else if (type === "status") {
|
|
606
|
-
projected["options"] = (payload?.options ?? []).map((option) => option?.name ?? null);
|
|
607
|
-
projected["groups"] = (payload?.groups ?? []).map((group) => group?.name ?? null);
|
|
608
|
-
}
|
|
609
|
-
else if (type === "relation") {
|
|
610
|
-
// Requests must send data_source_id; responses carry both. Give the caller
|
|
611
|
-
// the one it is allowed to write with.
|
|
612
|
-
projected["relation_data_source_id"] = payload?.data_source_id ?? null;
|
|
613
|
-
}
|
|
614
|
-
else if (type === "formula") {
|
|
615
|
-
projected["expression"] = payload?.expression ?? null;
|
|
616
|
-
}
|
|
617
|
-
else if (type === "rollup") {
|
|
618
|
-
projected["rollup"] = {
|
|
619
|
-
relation_property_name: payload?.relation_property_name ?? null,
|
|
620
|
-
rollup_property_name: payload?.rollup_property_name ?? null,
|
|
621
|
-
function: payload?.function ?? null,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
return projected;
|
|
625
|
-
}
|
|
626
|
-
// Shared schema fragments. The compact renderer inlines every property
|
|
627
|
-
// description, so these are paid for once per tool that uses them; the long
|
|
628
|
-
// versions live in the usage guide, which is fetched once rather than per tool
|
|
629
|
-
// ([#342](https://github.com/zackbart/connecta/issues/342)).
|
|
630
|
-
const RAW_PROPERTY = {
|
|
631
|
-
type: "boolean",
|
|
632
|
-
description: "Return Notion's much larger unprojected response instead of the lean projection.",
|
|
633
|
-
};
|
|
634
|
-
const PAGE_SIZE_PROPERTY = {
|
|
635
|
-
type: "integer",
|
|
636
|
-
minimum: 1,
|
|
637
|
-
maximum: MAX_PAGE_SIZE,
|
|
638
|
-
description: `Results per page (1-${MAX_PAGE_SIZE}). Defaults to the connector's configured page size.`,
|
|
639
|
-
};
|
|
640
|
-
const START_CURSOR_PROPERTY = {
|
|
641
|
-
type: "string",
|
|
642
|
-
description: "Opaque next_cursor from the previous response. Pass it back verbatim.",
|
|
643
|
-
};
|
|
644
|
-
const PROPERTY_SELECT = {
|
|
645
|
-
type: "array",
|
|
646
|
-
items: { type: "string" },
|
|
647
|
-
description: "Return only these property names. Omit for all. The cheapest way to shrink a result.",
|
|
648
|
-
};
|
|
649
|
-
function listOutputSchema(itemSchema) {
|
|
650
|
-
return {
|
|
651
|
-
type: "object",
|
|
652
|
-
properties: {
|
|
653
|
-
results: { type: "array", items: itemSchema },
|
|
654
|
-
has_more: { type: "boolean", description: "True when another page exists." },
|
|
655
|
-
next_cursor: { type: ["string", "null"], description: "Pass as start_cursor to fetch the next page." },
|
|
656
|
-
},
|
|
657
|
-
required: ["results", "has_more", "next_cursor"],
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
const PAGE_OUTPUT_SCHEMA = {
|
|
661
|
-
type: "object",
|
|
662
|
-
description: "Projected page. With raw: true this is Notion's full page object instead.",
|
|
663
|
-
properties: {
|
|
664
|
-
id: { type: "string" },
|
|
665
|
-
object: { type: "string" },
|
|
666
|
-
title: { type: "string", description: "Plain text of the title property." },
|
|
667
|
-
url: { type: ["string", "null"] },
|
|
668
|
-
parent: {
|
|
669
|
-
type: ["object", "null"],
|
|
670
|
-
properties: { type: { type: "string" }, id: { type: ["string", "null"] } },
|
|
671
|
-
required: ["type", "id"],
|
|
672
|
-
},
|
|
673
|
-
icon: { type: ["string", "null"], description: "Emoji, native icon name, :custom_emoji:, or icon URL." },
|
|
674
|
-
created_time: { type: ["string", "null"] },
|
|
675
|
-
last_edited_time: { type: ["string", "null"] },
|
|
676
|
-
created_by: { type: ["object", "null"] },
|
|
677
|
-
last_edited_by: { type: ["object", "null"] },
|
|
678
|
-
in_trash: { type: "boolean" },
|
|
679
|
-
is_archived: { type: "boolean" },
|
|
680
|
-
properties: {
|
|
681
|
-
type: "object",
|
|
682
|
-
description: "Property name to flattened value: text for title/rich_text, name for select/status, array of names for multi_select, array of page ids for relation.",
|
|
683
|
-
},
|
|
684
|
-
truncated_properties: {
|
|
685
|
-
type: "array",
|
|
686
|
-
items: {
|
|
687
|
-
type: "object",
|
|
688
|
-
properties: {
|
|
689
|
-
name: { type: "string" },
|
|
690
|
-
id: { type: ["string", "null"], description: "The property_id in the property-item path." },
|
|
691
|
-
},
|
|
692
|
-
required: ["name", "id"],
|
|
693
|
-
},
|
|
694
|
-
description: "Properties Notion truncated at 25 entries. Read each in full with notion_api_read GET /v1/pages/{page_id}/properties/{property_id}.",
|
|
695
|
-
},
|
|
696
|
-
},
|
|
697
|
-
required: ["id", "title", "properties"],
|
|
698
|
-
};
|
|
699
|
-
const BLOCK_OUTPUT_SCHEMA = {
|
|
700
|
-
type: "object",
|
|
701
|
-
properties: {
|
|
702
|
-
id: { type: "string" },
|
|
703
|
-
type: { type: "string" },
|
|
704
|
-
depth: { type: "integer", description: "0 for direct children, 1 for their children, and so on." },
|
|
705
|
-
text: { type: "string", description: "Plain text of the block." },
|
|
706
|
-
has_children: { type: "boolean" },
|
|
707
|
-
checked: { type: "boolean", description: "to_do blocks only." },
|
|
708
|
-
language: { type: ["string", "null"], description: "code blocks only." },
|
|
709
|
-
url: { type: ["string", "null"], description: "Media and link blocks." },
|
|
710
|
-
cells: { type: "array", items: { type: "string" }, description: "table_row blocks only." },
|
|
711
|
-
icon: { type: ["string", "null"], description: "callout blocks only." },
|
|
712
|
-
raw: {
|
|
713
|
-
type: "object",
|
|
714
|
-
description: "The block type's untouched payload, present only for types this projection does not model and whose content is not plain rich text.",
|
|
715
|
-
},
|
|
716
|
-
},
|
|
717
|
-
required: ["id", "type", "depth", "text", "has_children"],
|
|
718
|
-
};
|
|
719
|
-
function resolvePageSize(requested, fallback) {
|
|
720
|
-
if (typeof requested === "number" && Number.isFinite(requested)) {
|
|
721
|
-
return Math.min(Math.max(Math.trunc(requested), 1), MAX_PAGE_SIZE);
|
|
722
|
-
}
|
|
723
|
-
return fallback;
|
|
724
|
-
}
|
|
725
|
-
function listEnvelope(payload, results) {
|
|
726
|
-
return { results, has_more: payload?.has_more === true, next_cursor: payload?.next_cursor ?? null };
|
|
727
|
-
}
|
|
728
|
-
function mappedListEnvelope(payload, project) {
|
|
729
|
-
return listEnvelope(payload, (payload?.results ?? []).map(project));
|
|
730
|
-
}
|
|
731
|
-
function pagination(args, defaultPageSize) {
|
|
732
|
-
return {
|
|
733
|
-
page_size: resolvePageSize(args.page_size, defaultPageSize),
|
|
734
|
-
...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
/** Both page GETs and data-source POST queries filter properties on the URL. */
|
|
738
|
-
function propertyQuery(properties, maxFilters) {
|
|
739
|
-
if (properties === undefined)
|
|
740
|
-
return {};
|
|
741
|
-
// The title column's name varies, but its id is always `title`. Fetch it
|
|
742
|
-
// even when projection drops the column so the top-level title survives.
|
|
743
|
-
const filters = [...new Set([...properties, "title"])];
|
|
744
|
-
// Filtering is an optimization. An oversized selection still needs every
|
|
745
|
-
// requested column, so retrieve all properties and keep projecting locally.
|
|
746
|
-
return maxFilters !== undefined && filters.length > maxFilters ? {} : { filter_properties: filters };
|
|
747
|
-
}
|
|
748
|
-
/** Exactly-one-of validation, phrased so the agent knows what to send next. */
|
|
749
|
-
function requireExactlyOne(provided, hint) {
|
|
750
|
-
const present = provided.filter(([, value]) => value !== undefined && value !== null && value !== "");
|
|
751
|
-
if (present.length !== 1) {
|
|
752
|
-
invalid(`Provide exactly one of ${provided.map(([name]) => name).join(", ")}. ${hint}`);
|
|
753
|
-
}
|
|
754
|
-
return present[0];
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* The named tools. Each earns its catalog bytes over the generic set (H14):
|
|
758
|
-
* the reads flatten Notion's wrapped properties, rich-text runs, and blocks
|
|
759
|
-
* (and walk nesting under a request ceiling); the two writes author Notion's
|
|
760
|
-
* rich-text and block shapes from plain text. Thin wrappers the generic tools
|
|
761
|
-
* replace one for one (database, users, comments, property items, property
|
|
762
|
-
* updates, trash) are gone; the guide names their operations.
|
|
763
|
-
*/
|
|
764
|
-
function namedTools(defaultPageSize) {
|
|
765
|
-
return [
|
|
766
|
-
{
|
|
767
|
-
name: "integration_search",
|
|
768
|
-
description: "Find pages and data sources by title across everything shared with this integration. Never searches page content — use integration_query_data_source for rows inside a database. Returns identity fields only.",
|
|
769
|
-
annotations: { readOnlyHint: true },
|
|
770
|
-
inputSchema: {
|
|
771
|
-
type: "object",
|
|
772
|
-
required: [],
|
|
773
|
-
properties: {
|
|
774
|
-
query: {
|
|
775
|
-
type: "string",
|
|
776
|
-
description: "Title substring to match. Omit to list everything shared with the integration.",
|
|
777
|
-
},
|
|
778
|
-
object_type: {
|
|
779
|
-
type: "string",
|
|
780
|
-
enum: ["page", "data_source"],
|
|
781
|
-
description: "Restrict results to pages or to data sources. Omit for both.",
|
|
782
|
-
},
|
|
783
|
-
sort: {
|
|
784
|
-
type: "string",
|
|
785
|
-
enum: ["last_edited_desc", "last_edited_asc", "relevance"],
|
|
786
|
-
description: "Result ordering. Defaults to Notion's relevance order.",
|
|
787
|
-
},
|
|
788
|
-
page_size: PAGE_SIZE_PROPERTY,
|
|
789
|
-
start_cursor: START_CURSOR_PROPERTY,
|
|
790
|
-
raw: RAW_PROPERTY,
|
|
791
|
-
},
|
|
792
|
-
additionalProperties: false,
|
|
793
|
-
},
|
|
794
|
-
outputSchema: listOutputSchema({
|
|
795
|
-
type: "object",
|
|
796
|
-
properties: {
|
|
797
|
-
id: { type: "string" },
|
|
798
|
-
object: { type: "string", description: '"page" or "data_source".' },
|
|
799
|
-
title: { type: "string" },
|
|
800
|
-
url: { type: ["string", "null"] },
|
|
801
|
-
parent: { type: ["object", "null"] },
|
|
802
|
-
database_id: { type: ["string", "null"], description: "Data source hits only: the containing database." },
|
|
803
|
-
last_edited_time: { type: ["string", "null"] },
|
|
804
|
-
},
|
|
805
|
-
required: ["id", "object", "title"],
|
|
806
|
-
}),
|
|
807
|
-
handler: async (args, ctx) => {
|
|
808
|
-
const body = pagination(args, defaultPageSize);
|
|
809
|
-
if (args.query)
|
|
810
|
-
body["query"] = args.query;
|
|
811
|
-
if (args.object_type)
|
|
812
|
-
body["filter"] = { property: "object", value: args.object_type };
|
|
813
|
-
if (args.sort === "relevance") {
|
|
814
|
-
body["sort"] = { property: "relevance" };
|
|
815
|
-
}
|
|
816
|
-
else if (args.sort) {
|
|
817
|
-
body["sort"] = {
|
|
818
|
-
timestamp: "last_edited_time",
|
|
819
|
-
direction: args.sort === "last_edited_asc" ? "ascending" : "descending",
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
const payload = await notion(ctx, "POST", "/v1/search", { body });
|
|
823
|
-
if (args.raw)
|
|
824
|
-
return payload;
|
|
825
|
-
return mappedListEnvelope(payload, projectSearchHit);
|
|
826
|
-
},
|
|
827
|
-
},
|
|
828
|
-
{
|
|
829
|
-
name: "integration_get_page",
|
|
830
|
-
description: "Fetch one page's metadata and flattened property values by id. Returns the page's properties, not its body content — use integration_get_page_content for the blocks.",
|
|
831
|
-
annotations: { readOnlyHint: true },
|
|
832
|
-
inputSchema: {
|
|
833
|
-
type: "object",
|
|
834
|
-
properties: {
|
|
835
|
-
page_id: { type: "string", description: "Notion page id, with or without dashes." },
|
|
836
|
-
properties: PROPERTY_SELECT,
|
|
837
|
-
raw: RAW_PROPERTY,
|
|
838
|
-
},
|
|
839
|
-
required: ["page_id"],
|
|
840
|
-
additionalProperties: false,
|
|
841
|
-
},
|
|
842
|
-
outputSchema: PAGE_OUTPUT_SCHEMA,
|
|
843
|
-
handler: async (args, ctx) => {
|
|
844
|
-
const payload = await notion(ctx, "GET", `/v1/pages/${id(args.page_id)}`, {
|
|
845
|
-
query: propertyQuery(args.properties, MAX_PAGE_PROPERTY_FILTERS),
|
|
846
|
-
});
|
|
847
|
-
if (args.raw)
|
|
848
|
-
return payload;
|
|
849
|
-
return projectPage(payload, args.properties);
|
|
850
|
-
},
|
|
851
|
-
},
|
|
852
|
-
{
|
|
853
|
-
name: "integration_get_page_content",
|
|
854
|
-
// `raw: true` returns the requested level exactly as Notion sent it and
|
|
855
|
-
// does not walk nested children, so `depth` is ignored alongside it: a
|
|
856
|
-
// raw read of a deep page yields one level.
|
|
857
|
-
description: "Read a page's body as a flat list of blocks reduced to plain text. Each block keeps its id, type, and depth so it can be quoted, appended after, or drilled into. Nested content requires depth > 0.",
|
|
858
|
-
annotations: { readOnlyHint: true },
|
|
859
|
-
inputSchema: {
|
|
860
|
-
type: "object",
|
|
861
|
-
properties: {
|
|
862
|
-
block_id: {
|
|
863
|
-
type: "string",
|
|
864
|
-
description: "Page id, or any block id to read that block's children. A page id is a valid block id.",
|
|
865
|
-
},
|
|
866
|
-
depth: {
|
|
867
|
-
type: "integer",
|
|
868
|
-
minimum: 0,
|
|
869
|
-
maximum: 2,
|
|
870
|
-
description: "How many levels of nested children to follow. 0 (default) returns direct children only. Each level multiplies downstream requests.",
|
|
871
|
-
},
|
|
872
|
-
page_size: PAGE_SIZE_PROPERTY,
|
|
873
|
-
start_cursor: START_CURSOR_PROPERTY,
|
|
874
|
-
raw: {
|
|
875
|
-
...RAW_PROPERTY,
|
|
876
|
-
description: `${RAW_PROPERTY["description"]} It returns this one level exactly as Notion sent it and does not walk nested children, so depth is ignored alongside it — read a child block_id directly instead.`,
|
|
877
|
-
},
|
|
878
|
-
},
|
|
879
|
-
required: ["block_id"],
|
|
880
|
-
additionalProperties: false,
|
|
881
|
-
},
|
|
882
|
-
outputSchema: {
|
|
883
|
-
...listOutputSchema(BLOCK_OUTPUT_SCHEMA),
|
|
884
|
-
properties: {
|
|
885
|
-
...listOutputSchema(BLOCK_OUTPUT_SCHEMA)["properties"],
|
|
886
|
-
truncated: {
|
|
887
|
-
type: "boolean",
|
|
888
|
-
description: "True when the nested walk stopped at its request ceiling. Some descendants are missing; re-read a specific block_id to continue.",
|
|
889
|
-
},
|
|
890
|
-
},
|
|
891
|
-
required: ["results", "has_more", "next_cursor", "truncated"],
|
|
892
|
-
},
|
|
893
|
-
handler: async (args, ctx) => {
|
|
894
|
-
const top = await notion(ctx, "GET", `/v1/blocks/${id(args.block_id)}/children`, {
|
|
895
|
-
query: pagination(args, defaultPageSize),
|
|
896
|
-
});
|
|
897
|
-
if (args.raw)
|
|
898
|
-
return top;
|
|
899
|
-
const maxDepth = typeof args.depth === "number" ? args.depth : 0;
|
|
900
|
-
let spent = 1;
|
|
901
|
-
let truncated = false;
|
|
902
|
-
const results = [];
|
|
903
|
-
const walk = async (blocks, depth) => {
|
|
904
|
-
for (const block of blocks) {
|
|
905
|
-
results.push(projectBlock(block, depth));
|
|
906
|
-
if (depth >= maxDepth || block?.has_children !== true)
|
|
907
|
-
continue;
|
|
908
|
-
if (spent >= MAX_CONTENT_REQUESTS) {
|
|
909
|
-
truncated = true;
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
spent += 1;
|
|
913
|
-
const child = await notion(ctx, "GET", `/v1/blocks/${id(block.id)}/children`, {
|
|
914
|
-
query: { page_size: MAX_PAGE_SIZE },
|
|
915
|
-
});
|
|
916
|
-
// Nested levels take their first page only; a block with more than
|
|
917
|
-
// 100 children is re-read directly rather than paged here.
|
|
918
|
-
if (child["has_more"] === true)
|
|
919
|
-
truncated = true;
|
|
920
|
-
await walk(child["results"] ?? [], depth + 1);
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
await walk(top["results"] ?? [], 0);
|
|
924
|
-
return { ...listEnvelope(top, results), truncated };
|
|
925
|
-
},
|
|
926
|
-
},
|
|
927
|
-
{
|
|
928
|
-
name: "integration_get_data_source_schema",
|
|
929
|
-
description: "List a data source's properties with their ids, types, and select/status options. Read this before filtering, sorting, or writing — filters and property names that do not match the schema exactly are rejected.",
|
|
930
|
-
annotations: { readOnlyHint: true },
|
|
931
|
-
inputSchema: {
|
|
932
|
-
type: "object",
|
|
933
|
-
properties: {
|
|
934
|
-
data_source_id: {
|
|
935
|
-
type: "string",
|
|
936
|
-
description: "Data source id from integration_search or a database's data_sources, not a database id.",
|
|
937
|
-
},
|
|
938
|
-
raw: RAW_PROPERTY,
|
|
939
|
-
},
|
|
940
|
-
required: ["data_source_id"],
|
|
941
|
-
additionalProperties: false,
|
|
942
|
-
},
|
|
943
|
-
outputSchema: {
|
|
944
|
-
type: "object",
|
|
945
|
-
properties: {
|
|
946
|
-
id: { type: "string" },
|
|
947
|
-
name: { type: "string" },
|
|
948
|
-
database_id: { type: ["string", "null"] },
|
|
949
|
-
title_property: {
|
|
950
|
-
type: ["string", "null"],
|
|
951
|
-
description: "Name of the title-typed property. integration_create_page needs this to title a row.",
|
|
952
|
-
},
|
|
953
|
-
properties: {
|
|
954
|
-
type: "object",
|
|
955
|
-
description: "Property name to { id, type, options?, relation_data_source_id? }.",
|
|
956
|
-
},
|
|
957
|
-
},
|
|
958
|
-
required: ["id", "name", "properties"],
|
|
959
|
-
},
|
|
960
|
-
handler: async (args, ctx) => {
|
|
961
|
-
const payload = await notion(ctx, "GET", `/v1/data_sources/${id(args.data_source_id)}`);
|
|
962
|
-
if (args.raw)
|
|
963
|
-
return payload;
|
|
964
|
-
const properties = {};
|
|
965
|
-
let titleProperty = null;
|
|
966
|
-
for (const [name, property] of Object.entries((payload["properties"] ?? {}))) {
|
|
967
|
-
properties[name] = projectSchemaProperty(property);
|
|
968
|
-
if (property?.type === "title")
|
|
969
|
-
titleProperty = name;
|
|
970
|
-
}
|
|
971
|
-
return {
|
|
972
|
-
id: payload["id"] ?? null,
|
|
973
|
-
name: plainText(payload["title"]) || (payload["name"] ?? ""),
|
|
974
|
-
database_id: payload["parent"]?.database_id ?? null,
|
|
975
|
-
title_property: titleProperty,
|
|
976
|
-
properties,
|
|
977
|
-
};
|
|
978
|
-
},
|
|
979
|
-
},
|
|
980
|
-
{
|
|
981
|
-
name: "integration_query_data_source",
|
|
982
|
-
description: "List rows in a data source with optional filtering and sorting, returning each row's properties already flattened. Requires a data_source_id, never a database_id. Narrow with the properties argument to keep results small.",
|
|
983
|
-
annotations: { readOnlyHint: true },
|
|
984
|
-
inputSchema: {
|
|
985
|
-
type: "object",
|
|
986
|
-
properties: {
|
|
987
|
-
data_source_id: {
|
|
988
|
-
type: "string",
|
|
989
|
-
description: "Data source id from integration_search or a database's data_sources.",
|
|
990
|
-
},
|
|
991
|
-
filter: {
|
|
992
|
-
type: "object",
|
|
993
|
-
description: 'Notion filter object, passed through unchanged. Single condition: {"property":"Status","status":{"equals":"Done"}}. Compound: {"and":[...]} or {"or":[...]}. Property names must match integration_get_data_source_schema exactly.',
|
|
994
|
-
},
|
|
995
|
-
sorts: {
|
|
996
|
-
type: "array",
|
|
997
|
-
description: "Sort order, applied in sequence.",
|
|
998
|
-
items: {
|
|
999
|
-
type: "object",
|
|
1000
|
-
properties: {
|
|
1001
|
-
property: { type: "string", description: "Property name to sort by." },
|
|
1002
|
-
timestamp: {
|
|
1003
|
-
type: "string",
|
|
1004
|
-
enum: ["created_time", "last_edited_time"],
|
|
1005
|
-
description: "Sort by a timestamp instead of a property.",
|
|
1006
|
-
},
|
|
1007
|
-
direction: {
|
|
1008
|
-
type: "string",
|
|
1009
|
-
enum: ["ascending", "descending"],
|
|
1010
|
-
description: "Direction for this sort.",
|
|
1011
|
-
},
|
|
1012
|
-
},
|
|
1013
|
-
additionalProperties: false,
|
|
1014
|
-
},
|
|
1015
|
-
},
|
|
1016
|
-
properties: PROPERTY_SELECT,
|
|
1017
|
-
page_size: PAGE_SIZE_PROPERTY,
|
|
1018
|
-
start_cursor: START_CURSOR_PROPERTY,
|
|
1019
|
-
raw: RAW_PROPERTY,
|
|
1020
|
-
},
|
|
1021
|
-
required: ["data_source_id"],
|
|
1022
|
-
additionalProperties: false,
|
|
1023
|
-
},
|
|
1024
|
-
outputSchema: {
|
|
1025
|
-
...listOutputSchema(PAGE_OUTPUT_SCHEMA),
|
|
1026
|
-
properties: {
|
|
1027
|
-
...listOutputSchema(PAGE_OUTPUT_SCHEMA)["properties"],
|
|
1028
|
-
incomplete: {
|
|
1029
|
-
type: "boolean",
|
|
1030
|
-
description: "Present when Notion stopped this query at its 10,000-result cap: has_more is false but rows are missing. Narrow the filter.",
|
|
1031
|
-
},
|
|
1032
|
-
},
|
|
1033
|
-
},
|
|
1034
|
-
handler: async (args, ctx) => {
|
|
1035
|
-
const body = pagination(args, defaultPageSize);
|
|
1036
|
-
if (args.filter)
|
|
1037
|
-
body["filter"] = args.filter;
|
|
1038
|
-
if (args.sorts)
|
|
1039
|
-
body["sorts"] = args.sorts;
|
|
1040
|
-
const payload = await notion(ctx, "POST", `/v1/data_sources/${id(args.data_source_id)}/query`, {
|
|
1041
|
-
query: propertyQuery(args.properties),
|
|
1042
|
-
body,
|
|
1043
|
-
});
|
|
1044
|
-
if (args.raw)
|
|
1045
|
-
return payload;
|
|
1046
|
-
const rows = mappedListEnvelope(payload, (row) => projectPage(row, args.properties));
|
|
1047
|
-
return incomplete(payload) ? { ...rows, incomplete: true } : rows;
|
|
1048
|
-
},
|
|
1049
|
-
},
|
|
1050
|
-
{
|
|
1051
|
-
name: "integration_create_page",
|
|
1052
|
-
description: "Create a page, either as a child of another page or as a row in a data source. Notion has no idempotency key: a retried create makes a second page, so confirm with search before repeating one.",
|
|
1053
|
-
annotations: { readOnlyHint: false },
|
|
1054
|
-
inputSchema: {
|
|
1055
|
-
// A deliberate subset of the 2026-03-11 create contract, reviewed after
|
|
1056
|
-
// the 0.17.0 drift check (#408). Workspace-private pages, templates,
|
|
1057
|
-
// page placement, and the expanded icon/cover forms stay out: they
|
|
1058
|
-
// change ownership, start asynchronous content work, control ordering,
|
|
1059
|
-
// or depend on file surfaces, none of which extend page/row authoring.
|
|
1060
|
-
// notion_api_write reaches the full contract.
|
|
1061
|
-
required: [],
|
|
1062
|
-
type: "object",
|
|
1063
|
-
properties: {
|
|
1064
|
-
parent_page_id: {
|
|
1065
|
-
type: "string",
|
|
1066
|
-
description: "Create as a child page of this page. Exactly one parent id, this or parent_data_source_id.",
|
|
1067
|
-
},
|
|
1068
|
-
parent_data_source_id: {
|
|
1069
|
-
type: "string",
|
|
1070
|
-
description: "Create as a row in this data source, not a database id. Exactly one parent id, this or parent_page_id.",
|
|
1071
|
-
},
|
|
1072
|
-
title: { type: "string", description: "Plain-text title." },
|
|
1073
|
-
title_property: {
|
|
1074
|
-
type: "string",
|
|
1075
|
-
description: 'Name of the title-typed property, from integration_get_data_source_schema. Required in practice for a data-source parent, whose title column is rarely called "title". Defaults to "title", which is the only valid key under a page parent.',
|
|
1076
|
-
},
|
|
1077
|
-
properties: {
|
|
1078
|
-
type: "object",
|
|
1079
|
-
description: 'Additional Notion property values, keyed by property name and in Notion\'s own wrapped form, e.g. {"Status":{"status":{"name":"Todo"}}}. Read integration_get_data_source_schema first.',
|
|
1080
|
-
},
|
|
1081
|
-
markdown: {
|
|
1082
|
-
type: "string",
|
|
1083
|
-
description: "Page body as Notion-flavored Markdown. Mutually exclusive with children.",
|
|
1084
|
-
},
|
|
1085
|
-
children: {
|
|
1086
|
-
type: "array",
|
|
1087
|
-
maxItems: MAX_CHILDREN_PER_REQUEST,
|
|
1088
|
-
description: "Page body as raw Notion block objects. Mutually exclusive with markdown.",
|
|
1089
|
-
items: { type: "object" },
|
|
1090
|
-
},
|
|
1091
|
-
icon: { type: "string", description: "Emoji to use as the page icon." },
|
|
1092
|
-
},
|
|
1093
|
-
additionalProperties: false,
|
|
1094
|
-
},
|
|
1095
|
-
outputSchema: PAGE_OUTPUT_SCHEMA,
|
|
1096
|
-
handler: async (args, ctx) => {
|
|
1097
|
-
const [parentKey, parentValue] = requireExactlyOne([
|
|
1098
|
-
["parent_page_id", args.parent_page_id],
|
|
1099
|
-
["parent_data_source_id", args.parent_data_source_id],
|
|
1100
|
-
], "A page needs exactly one parent, and a data source is addressed by its data_source_id — never by a database_id.");
|
|
1101
|
-
if (args.markdown !== undefined && args.children !== undefined) {
|
|
1102
|
-
invalid("Provide either markdown or children for the page body, not both.");
|
|
1103
|
-
}
|
|
1104
|
-
const properties = { ...args.properties };
|
|
1105
|
-
if (args.title !== undefined)
|
|
1106
|
-
properties[args.title_property ?? "title"] = { title: richText(args.title, "title") };
|
|
1107
|
-
const body = {
|
|
1108
|
-
parent: parentKey === "parent_page_id"
|
|
1109
|
-
? { type: "page_id", page_id: parentValue }
|
|
1110
|
-
: { type: "data_source_id", data_source_id: parentValue },
|
|
1111
|
-
properties,
|
|
1112
|
-
};
|
|
1113
|
-
if (args.markdown !== undefined)
|
|
1114
|
-
body["markdown"] = args.markdown;
|
|
1115
|
-
if (args.children !== undefined)
|
|
1116
|
-
body["children"] = args.children;
|
|
1117
|
-
if (args.icon !== undefined)
|
|
1118
|
-
body["icon"] = { type: "emoji", emoji: args.icon };
|
|
1119
|
-
return projectPage(await notion(ctx, "POST", "/v1/pages", { body }));
|
|
1120
|
-
},
|
|
1121
|
-
},
|
|
1122
|
-
{
|
|
1123
|
-
name: "integration_append_blocks",
|
|
1124
|
-
description: "Append content to the end of a page or block, or insert it at a chosen position. Appending only adds: existing blocks are never moved or replaced, and an appended block cannot be relocated later through the API.",
|
|
1125
|
-
annotations: { readOnlyHint: false },
|
|
1126
|
-
inputSchema: {
|
|
1127
|
-
type: "object",
|
|
1128
|
-
properties: {
|
|
1129
|
-
block_id: { type: "string", description: "Page id or block id to append into." },
|
|
1130
|
-
text: {
|
|
1131
|
-
type: "array",
|
|
1132
|
-
items: { type: "string" },
|
|
1133
|
-
maxItems: MAX_CHILDREN_PER_REQUEST,
|
|
1134
|
-
description: "Plain-text paragraphs, one block each. Use exactly one of text, checklist, or children.",
|
|
1135
|
-
},
|
|
1136
|
-
checklist: {
|
|
1137
|
-
type: "array",
|
|
1138
|
-
items: { type: "string" },
|
|
1139
|
-
maxItems: MAX_CHILDREN_PER_REQUEST,
|
|
1140
|
-
description: "Plain-text to-do items, one unchecked to_do block each.",
|
|
1141
|
-
},
|
|
1142
|
-
children: {
|
|
1143
|
-
type: "array",
|
|
1144
|
-
items: { type: "object" },
|
|
1145
|
-
maxItems: MAX_CHILDREN_PER_REQUEST,
|
|
1146
|
-
description: "Raw Notion block objects, for anything paragraphs and to-dos cannot express.",
|
|
1147
|
-
},
|
|
1148
|
-
position: {
|
|
1149
|
-
type: "string",
|
|
1150
|
-
enum: ["end", "start", "after_block"],
|
|
1151
|
-
description: 'Where to insert. Defaults to "end". "after_block" requires after_block_id.',
|
|
1152
|
-
},
|
|
1153
|
-
after_block_id: {
|
|
1154
|
-
type: "string",
|
|
1155
|
-
description: 'Insert directly after this block when position is "after_block".',
|
|
1156
|
-
},
|
|
1157
|
-
},
|
|
1158
|
-
required: ["block_id"],
|
|
1159
|
-
additionalProperties: false,
|
|
1160
|
-
},
|
|
1161
|
-
outputSchema: {
|
|
1162
|
-
type: "object",
|
|
1163
|
-
properties: {
|
|
1164
|
-
appended: { type: "integer", description: "How many blocks were created." },
|
|
1165
|
-
results: { type: "array", items: BLOCK_OUTPUT_SCHEMA },
|
|
1166
|
-
},
|
|
1167
|
-
required: ["appended", "results"],
|
|
1168
|
-
},
|
|
1169
|
-
handler: async (args, ctx) => {
|
|
1170
|
-
const [kind, value] = requireExactlyOne([
|
|
1171
|
-
["text", args.text],
|
|
1172
|
-
["checklist", args.checklist],
|
|
1173
|
-
["children", args.children],
|
|
1174
|
-
], "Use text for plain paragraphs, checklist for to-do items, or children for raw Notion blocks.");
|
|
1175
|
-
const children = kind === "text"
|
|
1176
|
-
? value.map((line, at) => ({
|
|
1177
|
-
object: "block",
|
|
1178
|
-
type: "paragraph",
|
|
1179
|
-
paragraph: { rich_text: richText(line, `text[${at}]`) },
|
|
1180
|
-
}))
|
|
1181
|
-
: kind === "checklist"
|
|
1182
|
-
? value.map((line, at) => ({
|
|
1183
|
-
object: "block",
|
|
1184
|
-
type: "to_do",
|
|
1185
|
-
to_do: { rich_text: richText(line, `checklist[${at}]`), checked: false },
|
|
1186
|
-
}))
|
|
1187
|
-
: value;
|
|
1188
|
-
if (children.length === 0)
|
|
1189
|
-
invalid("Nothing to append: provide at least one block.");
|
|
1190
|
-
const body = { children };
|
|
1191
|
-
if (args.position === "after_block") {
|
|
1192
|
-
if (!args.after_block_id)
|
|
1193
|
-
invalid('position "after_block" requires after_block_id.');
|
|
1194
|
-
body["position"] = { type: "after_block", after_block: { id: args.after_block_id } };
|
|
1195
|
-
}
|
|
1196
|
-
else if (args.position) {
|
|
1197
|
-
body["position"] = { type: args.position };
|
|
1198
|
-
}
|
|
1199
|
-
const payload = await notion(ctx, "PATCH", `/v1/blocks/${id(args.block_id)}/children`, { body });
|
|
1200
|
-
const results = (payload["results"] ?? []).map((block) => projectBlock(block, 0));
|
|
1201
|
-
return { appended: results.length, results };
|
|
1202
|
-
},
|
|
1203
|
-
},
|
|
1204
|
-
];
|
|
1205
|
-
}
|
|
1206
|
-
/** Notion's generic and named REST tools and its credential test. */
|
|
1207
|
-
export function notionRest(defaultPageSize) {
|
|
1208
|
-
return {
|
|
1209
|
-
tools: [...restTools(VENDOR), ...namedTools(defaultPageSize)],
|
|
1210
|
-
async testCredential(value, ctx) {
|
|
1211
|
-
// Notion has no token-introspection endpoint for internal integrations;
|
|
1212
|
-
// identifying the bot is the cheapest call that proves the token is live.
|
|
1213
|
-
try {
|
|
1214
|
-
const payload = await notion({ ...ctx, credential: { get: async () => value, getAll: async () => ({ value }) } }, "GET", "/v1/users/me");
|
|
1215
|
-
const name = payload["bot"]?.workspace_name ?? payload["name"] ?? "Notion";
|
|
1216
|
-
return { ok: true, message: `Authenticated as ${name}.` };
|
|
1217
|
-
}
|
|
1218
|
-
catch (error) {
|
|
1219
|
-
return {
|
|
1220
|
-
ok: false,
|
|
1221
|
-
message: error instanceof ConnectorCallError ? error.message : "Notion rejected the token.",
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
},
|
|
1225
|
-
};
|
|
1226
|
-
}
|