@zackbart/connecta 0.36.0 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +1 -1
  3. package/bin/version.mjs +1 -1
  4. package/dist/http/hatch-paging.js +2 -2
  5. package/dist/http/paging.js +3 -88
  6. package/dist/http/provider.js +1 -2
  7. package/dist/http/tools.js +1 -23
  8. package/dist/program-writes.js +0 -1
  9. package/dist/providers/_shared/sensitive-fields.js +0 -16
  10. package/dist/providers/breeze/api.js +2 -2
  11. package/dist/providers/ccb/api.js +1 -1
  12. package/dist/providers/cloudflare/api.js +8 -8
  13. package/dist/providers/cloudflare/lib/config.d.ts +2 -1
  14. package/dist/providers/docs/api.js +1 -1
  15. package/dist/providers/drive/api.js +1 -1
  16. package/dist/providers/drive/lib/hatch.js +1 -1
  17. package/dist/providers/drive/tools.js +1 -1
  18. package/dist/providers/forms/api.js +1 -1
  19. package/dist/providers/forms/lib/hatch.js +1 -1
  20. package/dist/providers/forms/tools.js +1 -1
  21. package/dist/providers/github/api.js +1 -1
  22. package/dist/providers/github/lib/discovery.js +2 -2
  23. package/dist/providers/gmail/api.js +1 -1
  24. package/dist/providers/granola/api.js +155 -0
  25. package/dist/providers/granola/index.d.ts +10 -0
  26. package/dist/providers/granola/index.js +68 -0
  27. package/dist/providers/granola/openapi.generated.js +30 -0
  28. package/dist/providers/granola/safety.js +37 -0
  29. package/dist/providers/granola/skill.generated.js +5 -0
  30. package/dist/providers/granola/tools.js +2 -0
  31. package/dist/providers/overflow/api.js +26 -26
  32. package/dist/providers/planning-center/api.js +4 -4
  33. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  34. package/dist/providers/planning-center/lib/json-api.js +2 -2
  35. package/dist/providers/quickbooks/lib/query.js +1 -1
  36. package/dist/providers/quickbooks/safety.js +1 -1
  37. package/dist/providers/sheets/api.js +1 -1
  38. package/dist/providers/sheets/lib/hatch.js +2 -2
  39. package/dist/providers/sheets/lib/requests.js +1 -1
  40. package/dist/providers/slides/api.js +1 -1
  41. package/dist/providers/stripe/api.js +7 -7
  42. package/dist/providers/tithely/api.js +2 -2
  43. package/dist/providers/vercel/api.js +12 -12
  44. package/dist/providers/vercel/lib/pin.js +1 -1
  45. package/dist/routes/mcp.js +1 -1
  46. package/dist/runtime/admission.js +0 -12
  47. package/dist/storage/keys.js +2 -3
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/documentation/architecture.md +124 -74
  51. package/documentation/auth.md +9 -2
  52. package/documentation/code-mode.md +6 -6
  53. package/documentation/meta-tools.md +5 -2
  54. package/documentation/operator-tests.md +11 -2
  55. package/documentation/operator-ui.md +1 -2
  56. package/documentation/providers.md +110 -0
  57. package/package.json +6 -2
  58. package/templates/node/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.36.1 — 2026-10-27
6
+
7
+ Connecta 0.36.1 adds read-only Granola workspace API access and lets Eve agents select a pool before each model call. Pool changes keep guidance caches separate and prevent replay of parked calls against a different pool. Existing static-pool configuration and public provider tools keep their behavior.
8
+
9
+ The contributor checks now center on real Node/Workers, browser, persistence and installed-package journeys. Every PR runs browser, audit, Eve and Docker package checks. This patch requires no deployment configuration migration. Granola operators supply a workspace key; Eve pool selectors are optional.
10
+
11
+ QuickBooks invoice updates remain unavailable while their delivery contract is unresolved. The workload validation recipe is reproducible locally; live validation requires separate authorization and is not claimed by this release.
12
+
13
+ ### Added
14
+
15
+ - Let the Eve helper choose a pool before each model call with an app-owned selector, fence parked calls after pool changes, and cache instructions and failure backoff per endpoint. Instruction requests also accept a custom client name.
16
+ - Add a read-only Granola workspace API provider for meeting notes, details, paginated transcripts, folders and spaces, with encrypted shared credentials and the standard operation search/details/read surface.
17
+
18
+ ### Changed
19
+
20
+ - Add a locally reproducible Eve 0.84 workload fixture with independently verified audience-exchange controls and retained server cancellation receipts, and clarify that custom-audience workload tokens are refused. Live validation still requires separate authorization for disposable resources. Verify the standard 2 GiB Fluid resource preset and require confirmed cleanup even when evidence persistence fails.
21
+ - Replace invariant/spec test-title bookkeeping with executable evidence file
22
+ references and the existing stateful behavior journeys. Require installed-package,
23
+ generated-deployment and Docker smoke in the shared CI build job, with browser,
24
+ audit and real Eve compatibility on every PR. Remove the path classifier,
25
+ duplicate package workflow and completed migration's cross-commit exemption guard.
26
+ Keep focused runtime probes, provider contracts, build/declaration checks and the
27
+ single fail-closed `check` status.
28
+ - Remove completed provider-migration exemptions, legacy generator fixtures and
29
+ unused internal list/guide adapters. Consolidate contributor provider and release
30
+ workflows around the current standard without changing public tools or guides.
31
+ - Replace lower-layer operator, configuration and storage test mirrors with real browser, SQLite restart and deployed Node/Workers persistence journeys. Preserve focused SQL rollback, expiry, isolation, secrecy and cancellation probes.
32
+ - Replace redundant auth, write, privacy and runtime test matrices with real HTTP/MCP identity, one-send recovery, secrecy and socket-cancellation journeys. Require prompt downstream closure and admission release after client cancellation, before the ordinary request deadline. Retain focused security and lifetime fault probes without changing runtime behavior or public APIs.
33
+ - Clarify why QuickBooks invoice updates remain unavailable: sparse field preservation and request IDs do not establish email suppression for existing send-ready invoices. Record the authoritative contract gaps and a proposed unsent-only update for further review; no accounting write surface is added.
34
+ - Replace the remaining catalog-wide provider failure sweep and redundant
35
+ provider/HTTP agreement checks with shared stateful outcomes and real HTTP
36
+ failure/recovery journeys. Retain vendor-specific privacy, MIME, ownership,
37
+ revision, physical-send and lifetime fault probes; remove five internal paging
38
+ helpers used only by the retired tests and private exports hidden by the old
39
+ source scan. Require successful, complete delivered reconciliation data for the
40
+ Tithe.ly cancellation journey; failed, empty, malformed and incomplete replies
41
+ cannot establish recovery. Public provider behavior is unchanged.
42
+
43
+ ### Fixed
44
+
45
+ - Use permanently unreachable loopback port zero for native transport-failure probes so later test servers cannot answer them. Verify each platform's native connection error while preserving Node/Workers write-outcome, retry, reset and abort assertions.
46
+
5
47
  ## 0.36.0 — 2026-10-27
6
48
 
7
49
  Connecta 0.36.0 adds a maintained QuickBooks Online Accounting provider for one fixed sandbox or production company per connector. It offers company identity, bounded record searches, individual records and hierarchical financial reports, with contact details and payment credentials withheld. Deployments can opt into customer creation, sparse customer naming updates and unsent US service-invoice creation by setting `readOnly: false` on the connector. Writes need a stable request ID and are never replayed automatically; the API hatch stays read-only, and invoice updates remain unavailable. See [QuickBooks Online](https://github.com/zackbart/connecta/blob/v0.36.0/documentation/providers.md#quickbooks-online).
package/README.md CHANGED
@@ -24,7 +24,7 @@ Connecta supplies the service credentials, so the agent doesn't need them.
24
24
 
25
25
  ## What you can connect
26
26
 
27
- Maintained connections: <!-- providers:start -->Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Overflow, Planning Center, QuickBooks Online, Stripe, Tithe.ly, and Vercel<!-- providers:end -->.
27
+ Maintained connections: <!-- providers:start -->Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Granola, Overflow, Planning Center, QuickBooks Online, Stripe, Tithe.ly, and Vercel<!-- providers:end -->.
28
28
 
29
29
  Maintained API providers pair reviewed vendor contracts with tools built by
30
30
  Connecta. See the [API sources and tool inventory](./documentation/providers.md#api-sources-and-tool-inventory)
package/bin/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.36.0";
3
+ export const CONNECTA_VERSION = "0.36.1";
@@ -1,5 +1,5 @@
1
- // Internal opt-in adapters for the standard hatch. No maintained provider opts
2
- // in until its separate step-2 migration. Inputs have crossed connection safety.
1
+ // Internal opt-in adapters for the standard hatch. Inputs have crossed
2
+ // connection safety.
3
3
  import { ConnectorCallError } from "../errors.js";
4
4
  function record(value) {
5
5
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -1,29 +1,17 @@
1
1
  // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
2
2
  //
3
- // Paging is a strategy plus one cursor codec. A strategy turns "the next page
4
- // after this cursor" into a vendor request and reads back rows and the next
5
- // vendor cursor; tools take one page, and `allRows` walks every page as a
6
- // Stream for programs and the hatch. The codec is the only place a cursor is
7
- // minted: `plain` hands the vendor's cursor through; `bound` ties it to the tool and its
8
- // arguments so a cursor from another call is refused instead of silently
9
- // reading the wrong list.
10
- import { Effect, Option, Schema, Stream } from "effect";
3
+ // List tools take one page at a time. Lookahead detects an extra row; bound
4
+ // cursors authenticate the tool, arguments, connection and credential owner.
5
+ import { Effect, Schema } from "effect";
11
6
  import { ConnectorCallError } from "../errors.js";
12
7
  import { Refusal } from "./errors.js";
13
- import { confinementBase } from "../http-policy.js";
14
8
  import { canonicalJson } from "../canonical-json.js";
15
9
  import { ContinuationBinding, ContinuationStorage } from "../runtime/call.js";
16
10
  import { continuationKeys } from "../storage/keys.js";
17
- /** The `page` object every list tool returns. */
18
- export const PageInfo = Schema.Struct({
19
- hasMore: Schema.Boolean,
20
- nextCursor: Schema.NullOr(Schema.String),
21
- });
22
11
  /**
23
12
  * The standard `page` (decision 0011 S2): `nextCursor` is present exactly
24
13
  * when `hasMore` is true and continuation is safe. A hatch adapter can report
25
14
  * more data without a cursor when its continuation cannot be confined.
26
- * Step-1 lists keep `PageInfo` until their step 2.
27
15
  */
28
16
  export const StandardPage = Schema.Struct({
29
17
  hasMore: Schema.Boolean,
@@ -55,79 +43,6 @@ export function lookahead(options) {
55
43
  return { rows, page: { hasMore, nextCursor: id } };
56
44
  });
57
45
  }
58
- /** Vendor-supplied continuation, read only after the connection made its data safe. */
59
- export function token(options) {
60
- return Effect.flatMap(Effect.suspend(options.fetch), ({ rows, hasMore, nextCursor }) => {
61
- if (hasMore && !nextCursor)
62
- return badPage("The vendor reported another page without a next cursor.");
63
- return Effect.succeed({ rows, page: { hasMore, nextCursor: hasMore ? nextCursor : null } });
64
- });
65
- }
66
- /** Offset paging advances by the rows actually returned, with a total when supplied. */
67
- export function offset(options) {
68
- return Effect.gen(function* () {
69
- yield* position(options.offset, 0);
70
- yield* position(options.limit, 1);
71
- return yield* Effect.map(options.fetch(options.offset, options.limit), ({ rows, total }) => {
72
- const next = options.offset + rows.length;
73
- const hasMore = rows.length > 0 && (total === undefined ? rows.length >= options.limit : next < total);
74
- return { rows, page: { hasMore, nextCursor: hasMore ? String(next) : null } };
75
- });
76
- });
77
- }
78
- /** Page numbers from a vendor's safe body or headers, with full-page fallback. */
79
- export function page(options) {
80
- return Effect.gen(function* () {
81
- yield* position(options.page, 1);
82
- yield* position(options.limit, 1);
83
- return yield* Effect.map(options.fetch(options.page, options.limit), ({ rows, hasMore, totalPages }) => {
84
- const more = hasMore ?? (totalPages === undefined ? rows.length >= options.limit : options.page < totalPages);
85
- return { rows, page: { hasMore: more, nextCursor: more ? String(options.page + 1) : null } };
86
- });
87
- });
88
- }
89
- /** Extract only a cursor from a next URL confined to this connection's origin and path. */
90
- export function link(options) {
91
- const base = confinementBase("Paging", options.baseUrl);
92
- const prefix = base.pathname.replace(/\/+$/, "");
93
- if (!options.param)
94
- throw new Error("Link paging needs a cursor parameter.");
95
- return Effect.flatMap(Effect.suspend(options.fetch), ({ rows, next }) => {
96
- if (!next)
97
- return Effect.succeed({ rows, page: { hasMore: false, nextCursor: null } });
98
- let url;
99
- try {
100
- url = new URL(next, base);
101
- }
102
- catch {
103
- return badPage("The vendor returned an invalid next-page URL.");
104
- }
105
- if (url.origin !== base.origin ||
106
- !`${url.pathname}/`.startsWith(`${prefix}/`) ||
107
- url.username ||
108
- url.password ||
109
- url.hash)
110
- return badPage("The vendor returned a next-page URL outside this connection.");
111
- const cursors = url.searchParams.getAll(options.param);
112
- if (cursors.length !== 1 || !cursors[0])
113
- return badPage("The vendor returned no unique next-page cursor.");
114
- return Effect.succeed({ rows, page: { hasMore: true, nextCursor: cursors[0] } });
115
- });
116
- }
117
- function position(value, minimum) {
118
- return Number.isSafeInteger(value) && value >= minimum
119
- ? Effect.void
120
- : Effect.fail(new Refusal({
121
- error: new ConnectorCallError("invalid_args", "Paging positions and limits must be whole numbers within their bounds."),
122
- }));
123
- }
124
- function badPage(message) {
125
- return Effect.fail(new Refusal({ error: new ConnectorCallError("connector_call_failed", message, { retryable: false }) }));
126
- }
127
- /** Every row of every page, fetched as the stream is pulled. */
128
- export function allRows(fetchPage) {
129
- return Stream.paginate(undefined, (cursor) => Effect.map(fetchPage(cursor), ({ rows, page }) => [rows, Option.fromNullishOr(page.hasMore ? page.nextCursor : null)]));
130
- }
131
46
  const STRAY_CURSOR = "This cursor belongs to a different call. Pass back the nextCursor from the previous page of this same call, with the same arguments.";
132
47
  /** Resolved, nonextractable keys only: no request promise is retained between calls. */
133
48
  const signingKeys = new WeakMap();
@@ -191,8 +191,7 @@ export function defineApiProvider(definition) {
191
191
  // The declared fact the Writes convention states must match what the write builders send.
192
192
  if (idempotencyKey !== undefined && [...builtTools, ...hatchTools].some(takesIdempotencyKey) !== idempotencyKey)
193
193
  throw new Error(`${metadata.title} declares idempotencyKey: ${idempotencyKey}, but its write tools ${idempotencyKey ? "send no" : "send an"} idempotency key.`);
194
- const usageGuide = guide(options);
195
- const renderedGuide = "content" in usageGuide ? usageGuide : provider.usageGuide(usageGuide);
194
+ const renderedGuide = provider.usageGuide(guide(options));
196
195
  const connector = tracksEveryRequest(api(id, {
197
196
  ...(defaultCallAdmission
198
197
  ? {
@@ -22,7 +22,7 @@ import { causeError, runEdge } from "../runtime/run.js";
22
22
  import { recordUnauthorized, Refusal, RequestRefused, toConnectorCallError, withheldDefect, } from "./errors.js";
23
23
  import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
24
24
  import { catalogSchema, Integer } from "./json-schema.js";
25
- import { boundCursor, Cursor, PageInfo, StandardPage } from "./paging.js";
25
+ import { boundCursor, Cursor, StandardPage } from "./paging.js";
26
26
  import { Classification, Dispatches, IdempotencyKey, Recovery, ReadTool, WriteCapacity, writeRequestKey, fromConnectorContext, } from "./services.js";
27
27
  const inputs = new WeakMap();
28
28
  const keyed = new WeakSet();
@@ -118,28 +118,6 @@ export function get(connection, spec) {
118
118
  export function list(connection, spec) {
119
119
  if (spec.cursor !== undefined && spec.cursor !== "plain" && spec.cursor !== "bound")
120
120
  throw new Error('A list cursor must be "plain" or "bound".');
121
- if (!("key" in spec))
122
- return standardList(connection, spec);
123
- if (!spec.key || spec.key === "page")
124
- throw new Error("A list needs a row key distinct from page.");
125
- return read(connection, {
126
- ...spec,
127
- output: Schema.Struct({ [spec.key]: Schema.Array(spec.row), page: PageInfo }),
128
- run: (args) => Effect.gen(function* () {
129
- const fields = args;
130
- const supplied = typeof fields.cursor === "string" ? fields.cursor : undefined;
131
- const cursor = supplied !== undefined && spec.cursor === "bound"
132
- ? yield* boundCursor.decode(spec.name, fields, supplied)
133
- : supplied;
134
- const result = yield* spec.page(args, cursor);
135
- const nextCursor = result.page.nextCursor !== null && spec.cursor === "bound"
136
- ? yield* boundCursor.encode(spec.name, fields, result.page.nextCursor)
137
- : result.page.nextCursor;
138
- return { [spec.key]: result.rows, page: { ...result.page, nextCursor } };
139
- }),
140
- });
141
- }
142
- function standardList(connection, spec) {
143
121
  const { default: size, max } = spec.limit;
144
122
  if (spec.total !== undefined && spec.total !== true)
145
123
  throw new Error(`${spec.name} total must opt in with true for a vendor-stated exact count.`);
@@ -4,7 +4,6 @@ import { CONFIG_DEFAULTS } from "./config-defaults.js";
4
4
  import { Effect } from "effect";
5
5
  import { jsonResult } from "./meta-tools.js";
6
6
  import { classifyWriteOutcome } from "./write-outcome.js";
7
- export { classifyWriteOutcome } from "./write-outcome.js";
8
7
  /** Writes one program may send (`execute.maxWrites`), unless configured. */
9
8
  export const DEFAULT_MAX_WRITES = CONFIG_DEFAULTS.execute.maxWrites;
10
9
  /** What a dispatched write's outcome says about whether it landed. */
@@ -1,22 +1,6 @@
1
1
  // A provider's sensitive fields leave every result unless deployment code
2
2
  // returns them; no tool argument can. Shared by the providers whose raw rows
3
3
  // and hatches would otherwise reach what their projections drop.
4
- /** What `raw: true` and the hatches return, under the deployment's `sensitiveFields` choice. */
5
- export function unprojectedResults(sensitiveFields, names) {
6
- if (sensitiveFields === "return")
7
- return (body) => body;
8
- const dropped = new Set(names);
9
- const strip = (value) => {
10
- if (Array.isArray(value))
11
- return value.map(strip);
12
- if (!value || typeof value !== "object")
13
- return value;
14
- return Object.fromEntries(Object.entries(value)
15
- .filter(([name]) => !dropped.has(name))
16
- .map(([name, entry]) => [name, strip(entry)]));
17
- };
18
- return (body) => strip(body);
19
- }
20
4
  /** Refuse anything but the two choices, at construction (INV-11). */
21
5
  export function sensitiveFieldsOption(provider, value) {
22
6
  if (value === undefined)
@@ -66,7 +66,7 @@ const PATH_PATTERN = /^(\/[a-z0-9_]+)+\/?$/;
66
66
  * which reads the calendar without a key, and its embed key. Neither hatch
67
67
  * returns them.
68
68
  */
69
- export function withoutCalendarFeeds(path, body) {
69
+ function withoutCalendarFeeds(path, body) {
70
70
  if (path !== "/events/calendars/list")
71
71
  return body;
72
72
  const strip = (value) => {
@@ -80,7 +80,7 @@ export function withoutCalendarFeeds(path, body) {
80
80
  };
81
81
  return strip(body);
82
82
  }
83
- export function normalizedPath(raw) {
83
+ function normalizedPath(raw) {
84
84
  const path = typeof raw === "string" ? raw : "";
85
85
  if (!PATH_PATTERN.test(path)) {
86
86
  throw new ConnectorCallError("invalid_args", "A Breeze path is relative to /api, begins with '/', and uses only lowercase letters, digits, underscores, and '/' — for example /tags/list_folders. No query string, host, or dot segments.");
@@ -238,7 +238,7 @@ export const CCB_INDEX = new OperationIndex({
238
238
  export const getMe = get("getMe", "/me", { success: Schema.Unknown });
239
239
  export const CcbApi = indexedVendor("CCB", CCB_INDEX, [getMe]);
240
240
  const safety = valueSafety(CCB_VALUE_SAFETY, () => CCB_INDEX);
241
- export const CCB_PAGE_HEADERS = ["x-page", "x-next-page", "x-total-pages", "x-total"];
241
+ const CCB_PAGE_HEADERS = ["x-page", "x-next-page", "x-total-pages", "x-total"];
242
242
  export function ccbPageInfo(headers, page, size, rows) {
243
243
  const number = (name) => {
244
244
  const raw = headers[name];
@@ -157,7 +157,7 @@ export const UPLOADS = [
157
157
  * pin, zone creation must name a pinned account, and the rest is public data.
158
158
  * Every other unscoped operation is refused under a pin (default deny).
159
159
  */
160
- export const PIN_SAFE_UNSCOPED = new Set([
160
+ const PIN_SAFE_UNSCOPED = new Set([
161
161
  "GET /user",
162
162
  "GET /user/tokens/verify",
163
163
  "GET /accounts",
@@ -177,7 +177,7 @@ export const PIN_SAFE_UNSCOPED = new Set([
177
177
  "POST /graphql",
178
178
  ]);
179
179
  /** Why each family of unscoped operations is refused under a pin; `test` checks every one is covered. */
180
- export const PIN_REFUSED_UNSCOPED = {
180
+ const PIN_REFUSED_UNSCOPED = {
181
181
  accounts: "creates or moves accounts outside the pin",
182
182
  analytics: "queries datasets across every account; use POST /accounts/{account_tag}/analytics/sql",
183
183
  api: "is an internal health route",
@@ -445,18 +445,18 @@ export const cloudflareIndex = new CloudflareIndex(cloudflareDiscovery(openapi),
445
445
  title: "Cloudflare",
446
446
  slashParams: SLASH_PARAMS,
447
447
  });
448
- export const getUser = get("getUser", "/user", { success: Schema.Unknown });
449
- export const verifyToken = get("verifyToken", "/user/tokens/verify", { success: Schema.Unknown });
450
- export const verifyAccountToken = get("verifyAccountToken", "/accounts/:account_id/tokens/verify", {
448
+ const getUser = get("getUser", "/user", { success: Schema.Unknown });
449
+ const verifyToken = get("verifyToken", "/user/tokens/verify", { success: Schema.Unknown });
450
+ const verifyAccountToken = get("verifyAccountToken", "/accounts/:account_id/tokens/verify", {
451
451
  params: { account_id: Schema.String },
452
452
  success: Schema.Unknown,
453
453
  });
454
454
  const PageQuery = { page: Schema.optionalKey(Integer), per_page: Schema.optionalKey(Integer) };
455
- export const listAccounts = get("listAccounts", "/accounts", {
455
+ const listAccounts = get("listAccounts", "/accounts", {
456
456
  query: { ...PageQuery, name: Schema.optionalKey(Schema.String) },
457
457
  success: Schema.Unknown,
458
458
  });
459
- export const listZones = get("listZones", "/zones", {
459
+ const listZones = get("listZones", "/zones", {
460
460
  query: {
461
461
  ...PageQuery,
462
462
  name: Schema.optionalKey(Schema.String),
@@ -465,7 +465,7 @@ export const listZones = get("listZones", "/zones", {
465
465
  },
466
466
  success: Schema.Unknown,
467
467
  });
468
- export const graphqlQuery = post("graphqlQuery", "/graphql", {
468
+ const graphqlQuery = post("graphqlQuery", "/graphql", {
469
469
  payload: Schema.Struct({
470
470
  query: Schema.String,
471
471
  variables: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
@@ -1,7 +1,7 @@
1
1
  /** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
2
2
  export declare const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
3
3
  /** The two key schemes Cloudflare's v4 API accepts. */
4
- export type CloudflareKeyAuth = "apiToken" | "globalApiKey";
4
+ type CloudflareKeyAuth = "apiToken" | "globalApiKey";
5
5
  /** Accounts and zones a connector may touch; everything else is refused. */
6
6
  export interface CloudflarePin {
7
7
  accountIds?: readonly string[];
@@ -14,3 +14,4 @@ export interface CloudflareRestOptions {
14
14
  zoneId: string | undefined;
15
15
  pin: CloudflarePin | undefined;
16
16
  }
17
+ export {};
@@ -12,7 +12,7 @@ export const DOCS = {
12
12
  notFound: "ambiguous",
13
13
  };
14
14
  const DocumentId = { documentId: Schema.String };
15
- export const getDocument = get("getDocument", "/documents/:documentId", {
15
+ const getDocument = get("getDocument", "/documents/:documentId", {
16
16
  params: DocumentId,
17
17
  query: {
18
18
  includeTabsContent: Schema.optionalKey(Schema.String),
@@ -18,7 +18,7 @@ export const DRIVE = {
18
18
  };
19
19
  const FileId = { fileId: Schema.String };
20
20
  const PermissionId = { ...FileId, permissionId: Schema.String };
21
- export const getFile = get("getFile", "/files/:fileId", { params: FileId, success: Schema.Unknown });
21
+ const getFile = get("getFile", "/files/:fileId", { params: FileId, success: Schema.Unknown });
22
22
  const OPERATIONS = [
23
23
  get("listFiles", "/files", { success: Schema.Unknown }),
24
24
  post("createFile", "/files", { payload: Schema.Unknown, success: Schema.Unknown }),
@@ -8,7 +8,7 @@ export const driveIndex = new OperationIndex(openapi, {
8
8
  title: "Google Drive",
9
9
  pathMatching: "uri-template",
10
10
  });
11
- export const DRIVE_REFUSALS = {
11
+ const DRIVE_REFUSALS = {
12
12
  "GET /apps": "This operation has not been admitted to the narrow Drive execution allowlist; use the named file and permission workflows.",
13
13
  "GET /apps/{appId}": "This operation has not been admitted to the narrow Drive execution allowlist; use the named file and permission workflows.",
14
14
  "GET /changes": "This operation has not been admitted to the narrow Drive execution allowlist; use the named file and permission workflows.",
@@ -1138,7 +1138,7 @@ function guarded(tool) {
1138
1138
  }),
1139
1139
  };
1140
1140
  }
1141
- export function buildTool(connection, tool) {
1141
+ function buildTool(connection, tool) {
1142
1142
  const isRead = tool.annotations.readOnlyHint;
1143
1143
  const isList = ["search_files", "list_folder_items", "list_permissions", "list_shared_drives"].includes(tool.name);
1144
1144
  const output = asRecord(catalogSchema(tool.contract.output, "output"));
@@ -17,7 +17,7 @@ export const FORMS = {
17
17
  notFound: "ambiguous",
18
18
  };
19
19
  const FormId = { formId: Schema.String };
20
- export const getForm = get("getForm", "/forms/:formId", { params: FormId, success: Schema.Unknown });
20
+ const getForm = get("getForm", "/forms/:formId", { params: FormId, success: Schema.Unknown });
21
21
  const OPERATIONS = [
22
22
  getForm,
23
23
  post("createForm", "/forms", {
@@ -15,7 +15,7 @@ export const FORMS_REQUEST_KINDS = [
15
15
  "updateFormInfo",
16
16
  "updateSettings",
17
17
  ];
18
- export const FORMS_REFUSALS = {
18
+ const FORMS_REFUSALS = {
19
19
  "POST /forms/{formId}:setPublishSettings": "Publishing and response availability changes require separate product authorization; the owner can change them in Forms.",
20
20
  "POST /forms/{formId}/watches": "Watch registration creates persistent Pub/Sub delivery and requires separate product authorization.",
21
21
  "GET /forms/{formId}/watches": "Watch administration is outside this connection; manage watches through the approved operator workflow.",
@@ -16,7 +16,7 @@ import { googleBatchGuard } from "../_shared/google/hatch.js";
16
16
  const MAX_PAGE_SIZE = 100;
17
17
  const DEFAULT_PAGE_SIZE = 25;
18
18
  /** A listed answer is cut here; get_response returns it whole. */
19
- export const LIST_ANSWER_CHARS = 2_000;
19
+ const LIST_ANSWER_CHARS = 2_000;
20
20
  /** A form or item title or description is cut here in get_form's projection. */
21
21
  const DESCRIPTION_CHARS = 2_000;
22
22
  /** ...and bounded here in bytes, so escaped control characters cannot grow it. */
@@ -13,7 +13,7 @@ import { resultSafety } from "../../http/safety.js";
13
13
  import { connect, get, operations } from "../../http/vendor.js";
14
14
  const repository = { owner: Schema.String, repo: Schema.String };
15
15
  const success = Schema.Unknown;
16
- export const GitHubApi = indexedVendor("GitHub", githubIndex, [
16
+ const GitHubApi = indexedVendor("GitHub", githubIndex, [
17
17
  get("repository", "/repos/:owner/:repo", { params: repository, success }),
18
18
  ]);
19
19
  function rateLimited(status, retry, remaining) {
@@ -106,7 +106,7 @@ export const GITHUB_VERDICTS = {
106
106
  [MERGE]: "refuse",
107
107
  ...Object.fromEntries([...INTERNAL_READS].map((key) => [key, "refuse"])),
108
108
  };
109
- export const GITHUB_NAMED = {
109
+ const GITHUB_NAMED = {
110
110
  [MERGE]: "merge_pull_request",
111
111
  "GET /installation/repositories": "list_scopes",
112
112
  "GET /search/repositories": "search_scoped",
@@ -114,7 +114,7 @@ export const GITHUB_NAMED = {
114
114
  "GET /search/code": "search_scoped",
115
115
  };
116
116
  const adapter = githubPage("https://api.github.com");
117
- export const GITHUB_PAGES = Object.fromEntries(githubIndex.ops
117
+ const GITHUB_PAGES = Object.fromEntries(githubIndex.ops
118
118
  .filter((op) => {
119
119
  const key = `${op.method} ${op.path}`;
120
120
  return (READ_PERMISSIONS[key] && githubIndex.contract(op).parameters.some((p) => p.in === "query" && p.name === "page"));
@@ -22,7 +22,7 @@ const Page = {
22
22
  maxResults: Schema.optionalKey(Schema.String),
23
23
  pageToken: Schema.optionalKey(Schema.String),
24
24
  };
25
- export const listLabels = get("listLabels", "/labels", { success: Schema.Unknown });
25
+ const listLabels = get("listLabels", "/labels", { success: Schema.Unknown });
26
26
  const OPERATIONS = [
27
27
  get("listThreads", "/threads", {
28
28
  query: { ...Page, includeSpamTrash: Schema.optionalKey(Schema.String) },
@@ -0,0 +1,155 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { ConnectorCallError } from "../../errors.js";
3
+ import { Refusal, StatusFailure } from "../../http/errors.js";
4
+ import { Credentials } from "../../http/services.js";
5
+ import { restSafety } from "../../http/safety.js";
6
+ import { get } from "../../http/vendor.js";
7
+ import { indexedVendor } from "../_shared/rest/kit.js";
8
+ import { OperationIndex } from "../_shared/rest/operation-index.js";
9
+ import { valueSafety } from "../_shared/rest/value-safety.js";
10
+ import { openapi } from "./openapi.generated.js";
11
+ import { GRANOLA_VALUE_SAFETY, READS } from "./safety.js";
12
+ export const index = new OperationIndex(openapi, { vendor: "granola", title: "Granola" });
13
+ export const verdicts = Object.fromEntries(index.ops.map((op) => [
14
+ `${op.method} ${op.path}`,
15
+ READS.has(`${op.method} ${op.path}`) ? "read" : "refuse",
16
+ ]));
17
+ const listSpaces = get("listSpaces", "/v1/spaces", {
18
+ query: { page_size: Schema.optionalKey(Schema.Number) },
19
+ success: Schema.Unknown,
20
+ });
21
+ export const GranolaApi = indexedVendor("Granola", index, [listSpaces]);
22
+ const reviewed = valueSafety(GRANOLA_VALUE_SAFETY, () => index);
23
+ export const safety = () => ({
24
+ reviewed: restSafety(reviewed, index),
25
+ sensitiveFields: { names: [], choice: "drop" },
26
+ });
27
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
28
+ const printableToken = (value) => /^[!-~]+$/.test(value);
29
+ const page = {
30
+ param: "cursor",
31
+ in: "query",
32
+ decode(cursor) {
33
+ if (typeof cursor !== "string" ||
34
+ !cursor.trim() ||
35
+ [...cursor].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) ||
36
+ /^[a-z][a-z0-9+.-]*:/i.test(cursor))
37
+ throw new ConnectorCallError("invalid_args", "This cursor contains no valid Granola page token.");
38
+ return cursor;
39
+ },
40
+ next(data) {
41
+ const body = object(data);
42
+ if (body.hasMore === false && (body.cursor === null || body.cursor === undefined))
43
+ return null;
44
+ if (body.hasMore === true && typeof body.cursor === "string" && body.cursor.trim()) {
45
+ try {
46
+ return page.decode(body.cursor);
47
+ }
48
+ catch {
49
+ /* Fail closed on malformed vendor metadata. */
50
+ }
51
+ }
52
+ throw new ConnectorCallError("connector_call_failed", "Granola returned inconsistent paging metadata.", {
53
+ retryable: false,
54
+ });
55
+ },
56
+ };
57
+ export const pages = Object.fromEntries([...READS].filter((op) => op !== "GET /v1/notes/{note_id}").map((op) => [op, page]));
58
+ /** The compact index preserves bounds/formats but omits vendor regex patterns. */
59
+ export function binding({ connection }) {
60
+ return {
61
+ review: "Exact pinned note/folder/space identifier patterns before dispatch; standard JSON GET framing through Connection.sendPath.",
62
+ refuse(call) {
63
+ if (call.op.path.includes("{note_id}") && !/^\/v1\/notes\/not_[a-z0-9]{14}(?:\/transcript)?$/i.test(call.path))
64
+ return "Use a discovered Granola not_ API identifier with 14 alphanumeric characters, not a web-link UUID.";
65
+ for (const [key, prefix] of [
66
+ ["folder_id", "fol"],
67
+ ["space_id", "spa"],
68
+ ]) {
69
+ const value = call.query[key];
70
+ if (value !== undefined &&
71
+ (typeof value !== "string" || !new RegExp(`^${prefix}_[a-z0-9]{14}$`, "i").test(value)))
72
+ return `Use a discovered Granola ${prefix}_ API identifier for ${key}.`;
73
+ }
74
+ return undefined;
75
+ },
76
+ send: (call, options) => connection.sendPath({
77
+ method: call.method,
78
+ path: call.path,
79
+ template: call.op.path,
80
+ query: Object.entries(call.query).map(([key, value]) => [key, String(value)]),
81
+ }, options),
82
+ };
83
+ }
84
+ export const authenticate = Effect.gen(function* () {
85
+ const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
86
+ const key = values?.value?.trim();
87
+ if (!key || !printableToken(key))
88
+ return yield* Effect.fail(new Refusal({
89
+ error: new ConnectorCallError("auth_required", "An operator must configure a valid Granola workspace API key in this connection's credential slot."),
90
+ }));
91
+ return { Authorization: `Bearer ${key}` };
92
+ });
93
+ export const errors = {
94
+ provider: "Granola",
95
+ facts: () => ({}),
96
+ numericFacts: {
97
+ retryAfterMs: {
98
+ header: "retry-after",
99
+ format: "retry-after",
100
+ review: "HTTP Retry-After delay on rate-limited responses.",
101
+ },
102
+ },
103
+ meanings: { unknownId: "connector_call_failed", provesAbsence: false, conflicts: [] },
104
+ detail: ({ status }) => `Granola returned HTTP ${status}.`,
105
+ unreadable: "Granola returned an invalid JSON success body.",
106
+ statuses: {
107
+ 401: () => ({
108
+ code: "auth_required",
109
+ words: (detail) => `${detail} The workspace API key is invalid or revoked; an operator must replace it.`,
110
+ }),
111
+ 403: () => ({
112
+ code: "provider_permission_denied",
113
+ words: (detail) => `${detail} Check the workspace key's access and the space's Granola API integration.`,
114
+ }),
115
+ 404: () => ({
116
+ code: "connector_call_failed",
117
+ words: (detail) => `${detail} The note is inaccessible, missing, or has no generated summary/transcript; this does not prove absence.`,
118
+ }),
119
+ 413: () => ({
120
+ code: "connector_call_failed",
121
+ words: (detail) => `${detail} If include=transcript was requested, read /v1/notes/{note_id}/transcript in pages, then read note details without include. No automatic fallback or extra request was sent.`,
122
+ }),
123
+ 429: ({ retryAfterMs }) => ({
124
+ code: "rate_limited",
125
+ retryAfterMs: retryAfterMs ?? 5_000,
126
+ words: (detail) => `${detail} The workspace shares a burst of 25 requests per 5 seconds and a sustained 5 requests/second. Wait before retrying.`,
127
+ }),
128
+ },
129
+ };
130
+ /** Validate framing, independently of optional response fields and the caller's field selection. */
131
+ export const result = (body, answer) => {
132
+ const root = object(body);
133
+ const path = answer.operation.template;
134
+ const key = path === "/v1/notes"
135
+ ? "notes"
136
+ : path === "/v1/folders"
137
+ ? "folders"
138
+ : path === "/v1/spaces"
139
+ ? "spaces"
140
+ : path.endsWith("/transcript")
141
+ ? "transcript"
142
+ : undefined;
143
+ const valid = key
144
+ ? Array.isArray(root[key]) && typeof root.hasMore === "boolean"
145
+ : typeof root.id === "string" && /^not_[a-zA-Z0-9]{14}$/.test(root.id);
146
+ return valid
147
+ ? Effect.succeed(body)
148
+ : Effect.fail(new StatusFailure({ status: answer.status, headers: answer.headers, body: {}, operation: answer.operation }));
149
+ };
150
+ export const probe = {
151
+ operation: listSpaces,
152
+ request: { query: { page_size: 1 } },
153
+ check: (values) => !values.value?.trim() || !printableToken(values.value.trim()) ? "Enter a Granola workspace API key." : undefined,
154
+ describe: () => "Authenticated to the accessible Granola spaces. This does not prove access to every workspace note or verify the key's ownership type.",
155
+ };