@on-belay/sdk 2.2.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,274 @@
1
+ # Changelog — `@on-belay/sdk`
2
+
3
+ ## 3.1.0
4
+
5
+ ### Added
6
+
7
+ - **`anthropic.createMessage`** — call Claude through the On Belay proxy using the
8
+ **org's own** connected Anthropic key. No API key in your fieldset's environment,
9
+ no key management, and usage bills to the org rather than to the developer.
10
+
11
+ ```ts
12
+ import { anthropic } from "@on-belay/sdk"
13
+
14
+ const res = await anthropic.createMessage(orgId, {
15
+ body: { model: "claude-sonnet-5", max_tokens: 1024, messages: [{ role: "user", content: "..." }] },
16
+ })
17
+ ```
18
+
19
+ Add `create_message` on `anthropic` to your fieldset's `requiredOperations`
20
+ manifest, and the org must have the Anthropic integration connected.
21
+
22
+ ⚠️ **Streaming is not supported through the proxy.** The response is buffered and
23
+ returned whole — do not send `"stream": true` and expect SSE.
24
+
25
+ 🛑 On Belay's platform key is never used. An org that has not connected Anthropic
26
+ gets a clean failure rather than a silent fallback.
27
+
28
+ Helper count 1547 → 1548. No other helper changed; nothing removed. Purely additive,
29
+ so a `^3.0.0` range picks this up automatically.
30
+
31
+ ## 3.0.0 — 2026-08-23
32
+
33
+ **A major version by semver, with near-zero real migration work.** Two exported functions
34
+ are gone and 24 change arity, which is unambiguously major — but the 23 are helpers that
35
+ **404 on every call in 2.2.1**, so no working code depends on their current shape. If your
36
+ fieldset compiles against 2.2.1 today and does not call an Acumatica detail helper, it
37
+ compiles against 3.0.0 unchanged.
38
+
39
+ ### Why this release exists
40
+
41
+ `scripts/generate-integration-helpers.ts` imported only `prisma/operation-defs-part1` and
42
+ `-part2`, by name, while the operation catalog had grown to `part15` — so 330 operations
43
+ across 15 integrations were unreachable by construction. On top of that, the generator had
44
+ not been run in full since **2026-05-11** (SDK 2.2.0), so even operations added to part1 and
45
+ part2 after that date never reached the package. `shopmonkey.listInventoryParts` below is one
46
+ of those: it was catalogued on 2026-08-20 in *part2*, a file the generator did read.
47
+
48
+ Both gaps were invisible for the same reason: nothing compared the generated output to the
49
+ catalog. The only changes the generated directory received in those three months were two
50
+ single-file **hand-edits** (#825, #1059) to files whose header says
51
+ *"AUTO-GENERATED — do not edit by hand"*. Both happen to survive this regeneration — the
52
+ `get_video_analytics` deletion because its catalog row was deleted too, and the
53
+ `ShopifySlug` narrowing because it was folded into the generator — but that was luck, and
54
+ nothing was checking.
55
+
56
+ | | 2.2.1 | 3.0.0 |
57
+ |---|---:|---:|
58
+ | Operations exposed | 941 | **1,547** |
59
+ | Integration modules | 75 | **110** |
60
+
61
+ ### Added — 608 helpers, 35 new integration modules
62
+
63
+ `air`, `airtable`, `amazon-ads`, `applovin`, `box`, `celigo`, `cloudflare`, `cloudinary`,
64
+ `feishu`, `gemini`, `gooddata`, `google-business-profile`, `impact`, `instagram-insights`,
65
+ `instagram-public`, `judge-me`, `knocommerce`, `linkedin-pages`, `magento`, `microsoft-ads`,
66
+ `mixpanel`, `okta`, `openai-ads`, `pinterest-organic`, `railway`, `rippling`, `salsify`,
67
+ `semrush`, `sharepoint`, `shopmy`, `smile-io`, `teachable`, `tiktok-organic`,
68
+ `transcriptapi`, `web-search`, `windsor`.
69
+
70
+ 440 of the 606 net-new helpers are in those 35 modules; the other 166 landed on modules that
71
+ already existed. Two examples of what the gap cost in practice:
72
+
73
+ - **`shopmonkey.listInventoryParts`** (`/v3/inventory_part`). A production fieldset
74
+ hand-rolled a raw `executeProxyCall` around this, with the comment *"the SDK exposes only
75
+ a generic `listInventory` (→ /v3/inventory); the spec's op is `list_inventory_parts`
76
+ (→ /v3/inventory_part)"* — `owl-shopmonkey-import/src/lib/inventory-resolve.ts:233`. It is
77
+ a typed helper now, and that workaround can be deleted.
78
+ - **`amazonSeller.pullConnector`** (`/amazon_sp`). The only Amazon operation that returns
79
+ real Amazon data. The SDK previously exposed ten Amazon operations that the proxy silently
80
+ rewrites to Windsor aggregates, and not this one.
81
+
82
+ ### Fixed — 23 Acumatica helpers built a malformed URL and 404'd on every call
83
+
84
+ `analyzePathPattern` converts a regex `pathPattern` into a template literal. A trailing bare
85
+ character class with no quantifier — Acumatica's `[^?/]` — matched none of its substitutions,
86
+ so `split("/")` sheared the class and the emitted path ended in the literal text `[^`.
87
+
88
+ ```
89
+ 2.2.1 /entity/Default/${defaultId}/SalesOrder/[^
90
+ 3.0.0 /entity/Default/${endpointVersion}/SalesOrder/${salesOrderId}
91
+ ```
92
+
93
+ This shipped: `npm unpack @on-belay/sdk@2.2.1` shows the leaked `[^` in `dist/index.js`.
94
+ Affected every detail fetch/update/delete across `SalesOrder`, `Customer`, `Vendor`,
95
+ `StockItem`, `NonStockItem`, `PurchaseOrder`, `Invoice`, `Payment`, `Contact`, `Employee`,
96
+ `GeneralLedgerTransaction`, `Account`, `Bill`.
97
+
98
+ **Each of these helpers now takes one more argument** — the resource key that used to be
99
+ `[^`. That is the breaking change, and it is the fix.
100
+
101
+ Also fixed: `sharepoint.searchDrive` emitted `/root/searchq='[^']*'` instead of
102
+ `/root/search(q='${param1}')`. New in this release, so nothing depended on the broken form.
103
+
104
+ ### Fixed — 14 Google helpers dropped the `:action` suffix from the path
105
+
106
+ `ga4.run_report.*`, `gemini.generateContent`, `gemini.streamGenerateContent`,
107
+ `gemini.embedContent`, `gemini.countTokens` and others emitted the resource path with the
108
+ action suffix **missing**:
109
+
110
+ ```
111
+ 2.2.1 /v1beta/properties/${propertyId}
112
+ 3.0.0 /v1beta/properties/${propertyId}:runReport
113
+ ```
114
+
115
+ A path segment that merely *contained* the parameter placeholder was being collapsed down
116
+ to the placeholder, so anything else in that segment was thrown away. Unlike the Acumatica
117
+ case this leaves nothing regex-shaped behind, so it is invisible to a "did any regex
118
+ punctuation survive?" check — the path is simply, silently, a different endpoint. Our own
119
+ proxy rejects it too, because the proxy validates the request path against the same
120
+ `pathPattern`.
121
+
122
+ The same collapse was dropping the literal `v` from version segments, so 27 Google Ads and
123
+ Salesforce helpers emitted `/${version}/…` where the API needs `/v18/…`. Now
124
+ `/v${version}/…`.
125
+
126
+ ### Fixed — a regex quantifier ate a parameter
127
+
128
+ `judgeMe.getProduct` emitted the literal path `/products/-`. Its pattern is
129
+ `^/products/-?\d+`; the parameter was substituted, then the query-string strip cut from the
130
+ `-?` quantifier onward and took the parameter with it. Now `/products/${productId}` — and
131
+ the helper takes the `productId` argument it always should have.
132
+
133
+ ### Fixed — 10 helpers were unparseable TypeScript
134
+
135
+ 44 catalog keys are dotted (`shopify.themes.list`, `ga4.run_report.overall`). The generator
136
+ left the dots in the function name, emitting `export async function shopify.themes.list<T>(`.
137
+ Ten such keys were already reachable, so the generator produced a file that would not parse.
138
+ Identifiers are now sanitized (`shopifyThemesList`); the dotted string is unchanged as the
139
+ operation key sent to the proxy.
140
+
141
+ ### Not changed — Shopify REST paths still omit `.json`
142
+
143
+ Unchanged from 2.2.1. Recorded because it was changed and reverted twice during review, and
144
+ the reasoning is worth having in one place.
145
+
146
+ ```
147
+ 2.2.1 and 3.0.0 /admin/api/{version}/orders
148
+ catalog pattern ^/admin/api/[^/]+/orders\.json ← requires .json
149
+ ```
150
+
151
+ **Your calls are not failing.** Shopify accepts both forms — 154 json-less `/admin/api/`
152
+ calls over 30 days, **every one status 200**, zero failures. And the platform check that
153
+ *would* reject a json-less path is unreachable from a fieldset: it runs only for
154
+ group-access callers with an enforcing grant, and fieldset tokens never take that branch.
155
+
156
+ ⚠️ **The argument for the other choice, stated fairly:** all 17 of the catalog's
157
+ hand-curated Shopify `/admin/api/` `examplePath` values carry `.json`. That is real, and it
158
+ is why this flipped once. It was judged **documentation** divergence rather than evidence of
159
+ breakage — a 55-path behaviour change needs a functional reason, and there is none for this
160
+ package's consumers. Those 17 appear in `PATH-DIVERGENCE.json` as expected entries.
161
+
162
+ ### Changed — caller-supplies-the-path operations now take that path as a parameter
163
+
164
+ Three patterns are bare prefixes rather than fixed endpoints. `shopify.admin.fetch` says so
165
+ in its own description — *"The path is chosen by the caller, so this operation is broad"* —
166
+ and its example is `/admin/api/2026-01/shop.json`. The generator emitted the prefix with no
167
+ parameter, so the helper could not reach any resource:
168
+
169
+ ```
170
+ 2.2.1 shopifyAdminFetch(orgId, slug, options) → /admin/api/2024-01
171
+ 3.0.0 shopifyAdminFetch(orgId, slug, resourcePath, …) → /admin/api/2026-01/${resourcePath}
172
+ ```
173
+
174
+ 5 operations: `shopify.admin.fetch`, github `get_file_contents` / `create_update_file`,
175
+ cloudinary `get_asset` / `delete_asset`. All five are new in 3.0.0 except
176
+ `shopify.admin.fetch`, whose 2.2.1 form could not be used for anything.
177
+
178
+ ### Known — 5 operations emit a path their pattern rejects, and are still shipped
179
+
180
+ `gmail.searchMessages`, `youtube.getVideo`, `googleDrive.downloadFile`,
181
+ `microsoftTeams.listTeams`, `yotpo.getProductReviews`. Their patterns bake a
182
+ **caller-supplied query value** into the path (`\?q=`, `\?.*id=`, `\?alt=media`, `\?.*resourceProvisioningOptions`), which a path
183
+ template cannot express.
184
+
185
+ **They are shipped, deliberately.** `youtube.getVideo` emits `/youtube/v3/videos` and that
186
+ exact path has **3 live 200s** in the last 90 days — pass the id in `queryParams`. An earlier
187
+ draft of this release omitted several of these; that was reverted. The patterns are
188
+ over-specified, and loosening them is a catalog change tracked separately.
189
+
190
+ All divergences are listed in `src/integrations/PATH-DIVERGENCE.json`.
191
+
192
+ ### Fixed — Shopify Admin API version was `2024-01`
193
+
194
+ Over a year past Shopify's end-of-life for that version. Now `2026-01`, read from the
195
+ platform's own `SHOPIFY_API_VERSION` constant at generate time so the two cannot drift.
196
+
197
+ ### Breaking changes — complete list
198
+
199
+ **27 changes require a caller to change code. 24 of them are path fixes.**
200
+
201
+ | Change | Count | Source |
202
+ |---|---:|---|
203
+ | Acumatica detail helpers take one more argument | 22 | **This release** — the 404 fix |
204
+ | `github.getFileContents` / `createUpdateFile` take a `resourcePath` argument | 2 | **This release** — the bare-prefix fix |
205
+ | `shopify.getAnalyticsReport`: `options.queryParams` → `options.body` | 1 | Catalog |
206
+ | `shopmonkey.getOrderFees` removed | 1 | Catalog |
207
+ | `shopmonkey.listInventory` removed | 1 | Catalog |
208
+
209
+ The last three are **catalog-driven, not authored in the SDK** — upstream operation
210
+ definitions changed and the SDK had simply not been regenerated since. `getAnalyticsReport`
211
+ moved because Shopify **deleted** the REST `reports.json` resource, so the operation is a
212
+ GraphQL POST now. Both `shopmonkey` keys were deleted from the catalog because the endpoints
213
+ no longer exist; `/v3/order/{id}/fee` 404s.
214
+
215
+ **41 parameters were renamed. These are not breaking for positional calls** — arity and
216
+ types are unchanged, and every helper is called positionally. They are corrections to names
217
+ a developer would otherwise trust and get wrong:
218
+
219
+ - `defaultId` → `endpointVersion` (Acumatica). The segment after `/entity/Default/` is the
220
+ endpoint *version* — `Default` is the endpoint *name* — so the old name read as an
221
+ identifier of something called Default.
222
+ - `salesorderId` → `salesOrderId`, `inventoryitemId` → `inventoryItemId`,
223
+ `generalledgertransactionId` → `generalLedgerTransactionId`, `localpostId` → `localPostId`,
224
+ and similar across `acumatica`, `netsuite`, `linkedin-ads`, `gooddata`,
225
+ `google-business-profile`. Names were being derived from a lowercased string, collapsing
226
+ the word boundaries.
227
+ - `accountId` → `adAccountId` on six `redditAds` helpers (catalog-driven).
228
+
229
+ ### Known affected consumers: none
230
+
231
+ `owl-shopmonkey-import` is the only external consumer. It pins `^2.2.0`, so it will **not**
232
+ pick up 3.0.0 automatically, and it calls none of the removed or changed helpers — verified
233
+ against its source: it uses `shopify.listInventoryLevels` and raw `executeProxyCall`.
234
+
235
+ ### Guardrails added, so this cannot recur silently
236
+
237
+ - The generator reads `prisma/operation-defs.ts` — the same aggregate seeded into the live
238
+ `IntegrationOperation` table — and **fails loudly** if any `operation-defs-part*.ts` file
239
+ on disk holds operations the aggregate does not carry. A future `part16` that nobody wires
240
+ in aborts the run and names the missing keys.
241
+ - **A path-divergence report.** After conversion, a value is substituted for every
242
+ `${param}` and the concrete path is tested against the source `pathPattern`. Mismatches
243
+ are written to a committed `src/integrations/PATH-DIVERGENCE.json` and the set is **pinned
244
+ by the test suite**, so it cannot grow unnoticed. Current run: **1,547 of 1,547 emitted,
245
+ 0 omitted, 64 divergences recorded.**
246
+ 🚫 It is deliberately **not** a gate. It was one, briefly, and as a gate it removed 55
247
+ working Shopify helpers plus `youtube.getVideo` — whose "broken" path has three live
248
+ 200s. A divergence says the template and the pattern disagree; it does not say which is
249
+ wrong.
250
+ - Colliding helper names, module filenames, or barrel namespaces abort the run instead of
251
+ silently shadowing.
252
+ - CI asserts the generated output matches the catalog exactly, in both directions, plus
253
+ identifier validity, barrel uniqueness, regression pins on each non-trivial path idiom,
254
+ and the Shopify version mirror. The staleness that hid these operations for three months
255
+ is now a red build, not a silence.
256
+ - ⚠️ **The divergence check replaced an earlier "no regex punctuation survived" check,
257
+ which was blind in both directions** and is the reason three defect classes in this
258
+ release shipped in 2.2.1 at all. A *dropped* literal leaves nothing regex-shaped behind
259
+ (`:runReport` vanishing passed it cleanly), and `?` is both regex punctuation and an
260
+ ordinary URL character (so google_drive's correct path was refused). The round-trip
261
+ subsumes it and needs no list of known idioms, so a shape nobody has seen yet is still
262
+ caught.
263
+ - **The examplePath check is the primary path signal.** 616 ops carry a hand-curated
264
+ `examplePath`; the test asserts every emitted template can produce its own. It caught what
265
+ nothing else could — `shopify.admin.fetch`'s bare prefix passed the pattern round-trip,
266
+ because a prefix pattern matches trivially. Three blind spots in sequence: punctuation
267
+ missed deletions, the round-trip missed under-specification, the example caught it.
268
+ **17 known mismatches**, pinned: 15 shopify + 1 yotpo are the deliberate json-less form
269
+ above, and 1 is `cloudinary.uploadAssetRemote`, where the pattern is a 4-way alternation
270
+ (`^/(image|video|auto|raw)/upload`) collapsed to the first option.
271
+ - Real path defects are pinned by tests that encode what the **upstream API** requires,
272
+ not agreement with a regex: the `:action` suffix and `v` version literal (28,142 live
273
+ google_ads calls, 100% carrying both), acumatica's sheared character class, and
274
+ judge_me's `/products/-`. Evidence about the API is the authority — a pattern is not.
package/README.md CHANGED
@@ -375,7 +375,7 @@ import { executeProxyCall } from "@on-belay/sdk"
375
375
  const result = await executeProxyCall<{ products: Array<{ id: number; title: string }> }>(
376
376
  orgId,
377
377
  "shopify",
378
- "shopify.products.list",
378
+ "list_products",
379
379
  "/admin/api/2024-01/products.json?limit=1",
380
380
  { method: "GET" },
381
381
  { fieldsetSlug: "my-fieldset" },
@@ -395,6 +395,25 @@ console.log(result.data.products[0]?.title)
395
395
 
396
396
  The org must have the integration connected; your fieldset's `requiredOperations` must include `operationKey`; the org must be enrolled in your fieldset. Each of those gates returns a different `ProxyErrorCode`.
397
397
 
398
+ > **`operationKey` is the operation's own catalog key, matched by exact string
399
+ > equality.** `list_products`, `create_contact`, `shopify.themes.assets.update` —
400
+ > passed alongside a separate `integrationSlug` argument. It is **not** a
401
+ > resource-and-verb path: `"shopify.products.list"` is not a catalog key on anything
402
+ > and is refused. There is no normalization, aliasing or wildcarding: the key you
403
+ > pass must be byte-identical to the one in your `requiredOperations`, which must in
404
+ > turn be a real key on that integration. Some catalog keys contain dots of their
405
+ > own; paste those exactly as written. The
406
+ > [typed integration helpers](#typed-integration-helpers) are generated from the
407
+ > catalog, so using them removes this class of mistake entirely.
408
+ >
409
+ > On the **application form** the pair is written as one string,
410
+ > `integrationSlug.operationKey` — e.g. `shopify.list_products`. A few catalog keys
411
+ > are themselves slug-prefixed, so an entry like `reddit_ads.get_report` reads two
412
+ > ways (the key `reddit_ads.get_report`, or the key `get_report` on `reddit_ads` —
413
+ > two different operations, with different path patterns). Those are rejected rather
414
+ > than guessed; write them with a colon — `reddit_ads:get_report` — which is never
415
+ > ambiguous.
416
+
398
417
  ### `getOrgContext`
399
418
 
400
419
  ```ts
@@ -669,7 +688,7 @@ class OnbelayClient {
669
688
  isEnrolled(orgId: string): Promise<boolean>
670
689
  getEnrolledOrgs(): Promise<string[]>
671
690
 
672
- shopify(orgId: string, integrationSlug?: "shopify" | "shopify_temp"): ShopifySubClient
691
+ shopify(orgId: string): ShopifySubClient
673
692
  // + one accessor per integration (hubspot, klaviyo, linear, …)
674
693
  }
675
694
  ```
@@ -727,7 +746,7 @@ The full type surface re-exported from the package root:
727
746
  | `ContentType` | Free-form string alias used by `recordPublish`. |
728
747
  | `PublishResult` | `recordPublish` return shape. |
729
748
  | `LogActionResult` | `logAction` return shape (`{ ok: boolean }`). |
730
- | `ShopifySlug` | `"shopify" \| "shopify_temp"` — accepted by the Shopify integration helpers. |
749
+ | `ShopifySlug` | `"shopify"` — accepted by the Shopify integration helpers. |
731
750
  | `WebhookPayload` | Inbound webhook body. See [§8](#8-webhook-payload-contract). |
732
751
  | `WebhookHandlerOptions`, `WebhookHandlerContext`, `WebhookResult` | `createOnbelayWebhookHandler` shapes. |
733
752
  | `WebhookVerifyOptions` | Options for `validateWebhookSignature` / handler. |
@@ -820,9 +839,27 @@ The full webhook contract is specified in `qa-brightline-dod-external-fieldset-s
820
839
  If you ship an embedded UI, the platform renders it as an iframe at `/dashboard/fieldsets/[slug]` with `sandbox="allow-scripts allow-forms"`. The handshake:
821
840
 
822
841
  1. Iframe loads. It posts `{ type: "onbelay:ready" }` to `https://app.onbelay.ai`.
823
- 2. Platform issues a 15-minute HS256 JWT and posts `{ type: "onbelay:context", token, orgId, userId }` back to the iframe.
842
+ 2. Platform issues a **1-hour** HS256 JWT and posts `{ type: "onbelay:context", token, proxyUrl, fieldsetSlug }` back to the iframe. There is **no top-level `orgId` or `userId`** — both are inside the JWT and are returned by `validateDashboardToken`. `proxyUrl` is a **base** URL (e.g. `https://app.onbelay.ai`), the same shape as `ONBELAY_PROXY_URL` and `OnbelayConfig.proxyUrl` — the SDK appends `/api/sdk/proxy` itself, so do **not** append it yourself or you will request `.../api/sdk/proxy/api/sdk/proxy`.
824
843
  3. Iframe sends `token` to **its own backend**, which calls `validateDashboardToken(token, ONBELAY_DASHBOARD_SECRET)` and then performs proxy calls scoped to the validated `orgId`.
825
- 4. On `visibilitychange`, the platform re-issues a fresh token.
844
+ 4. The platform re-issues a fresh token on `visibilitychange` **and** on a ~10-minute timer, so your listener must tolerate repeated `onbelay:context` messages — each supersedes the last.
845
+
846
+ > ⚠️ **Do not write the guard yourself — use `isOnbelayContextMessage`.**
847
+ >
848
+ > ```ts
849
+ > import { isOnbelayContextMessage } from "@on-belay/sdk"
850
+ >
851
+ > window.addEventListener("message", (event) => {
852
+ > if (event.origin !== "https://app.onbelay.ai") return
853
+ > if (!isOnbelayContextMessage(event.data)) return
854
+ > // event.data is narrowed: { type, token, proxyUrl, fieldsetSlug }
855
+ > })
856
+ > ```
857
+ >
858
+ > A hand-written guard of the shape `if (!d.token || !d.orgId) return` rejects
859
+ > **every** message, because `orgId` is not at the top level. The symptom is not an
860
+ > error — the UI renders and every API call becomes a silent no-op. This is a real
861
+ > incident, not a hypothetical: it is what happened on `owl-shopmonkey-import`
862
+ > (`docs/fieldsets/owl-shopmonkey-import/specs/qa-test-pass.md:81`).
826
863
 
827
864
  ```ts
828
865
  // Inside your iframe (browser):
@@ -853,7 +890,7 @@ if (!ctx) return new Response(JSON.stringify({ error: "invalid_token" }), { stat
853
890
  const result = await executeProxyCall(
854
891
  ctx.orgId,
855
892
  "shopify",
856
- "shopify.products.list",
893
+ "list_products",
857
894
  "/admin/api/2024-01/products.json",
858
895
  undefined,
859
896
  { fieldsetSlug: ctx.fieldsetSlug },