@on-belay/sdk 2.2.1 → 3.0.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,246 @@
1
+ # Changelog — `@on-belay/sdk`
2
+
3
+ ## 3.0.0 — 2026-08-23
4
+
5
+ **A major version by semver, with near-zero real migration work.** Two exported functions
6
+ are gone and 24 change arity, which is unambiguously major — but the 23 are helpers that
7
+ **404 on every call in 2.2.1**, so no working code depends on their current shape. If your
8
+ fieldset compiles against 2.2.1 today and does not call an Acumatica detail helper, it
9
+ compiles against 3.0.0 unchanged.
10
+
11
+ ### Why this release exists
12
+
13
+ `scripts/generate-integration-helpers.ts` imported only `prisma/operation-defs-part1` and
14
+ `-part2`, by name, while the operation catalog had grown to `part15` — so 330 operations
15
+ across 15 integrations were unreachable by construction. On top of that, the generator had
16
+ not been run in full since **2026-05-11** (SDK 2.2.0), so even operations added to part1 and
17
+ part2 after that date never reached the package. `shopmonkey.listInventoryParts` below is one
18
+ of those: it was catalogued on 2026-08-20 in *part2*, a file the generator did read.
19
+
20
+ Both gaps were invisible for the same reason: nothing compared the generated output to the
21
+ catalog. The only changes the generated directory received in those three months were two
22
+ single-file **hand-edits** (#825, #1059) to files whose header says
23
+ *"AUTO-GENERATED — do not edit by hand"*. Both happen to survive this regeneration — the
24
+ `get_video_analytics` deletion because its catalog row was deleted too, and the
25
+ `ShopifySlug` narrowing because it was folded into the generator — but that was luck, and
26
+ nothing was checking.
27
+
28
+ | | 2.2.1 | 3.0.0 |
29
+ |---|---:|---:|
30
+ | Operations exposed | 941 | **1,547** |
31
+ | Integration modules | 75 | **110** |
32
+
33
+ ### Added — 608 helpers, 35 new integration modules
34
+
35
+ `air`, `airtable`, `amazon-ads`, `applovin`, `box`, `celigo`, `cloudflare`, `cloudinary`,
36
+ `feishu`, `gemini`, `gooddata`, `google-business-profile`, `impact`, `instagram-insights`,
37
+ `instagram-public`, `judge-me`, `knocommerce`, `linkedin-pages`, `magento`, `microsoft-ads`,
38
+ `mixpanel`, `okta`, `openai-ads`, `pinterest-organic`, `railway`, `rippling`, `salsify`,
39
+ `semrush`, `sharepoint`, `shopmy`, `smile-io`, `teachable`, `tiktok-organic`,
40
+ `transcriptapi`, `web-search`, `windsor`.
41
+
42
+ 440 of the 606 net-new helpers are in those 35 modules; the other 166 landed on modules that
43
+ already existed. Two examples of what the gap cost in practice:
44
+
45
+ - **`shopmonkey.listInventoryParts`** (`/v3/inventory_part`). A production fieldset
46
+ hand-rolled a raw `executeProxyCall` around this, with the comment *"the SDK exposes only
47
+ a generic `listInventory` (→ /v3/inventory); the spec's op is `list_inventory_parts`
48
+ (→ /v3/inventory_part)"* — `owl-shopmonkey-import/src/lib/inventory-resolve.ts:233`. It is
49
+ a typed helper now, and that workaround can be deleted.
50
+ - **`amazonSeller.pullConnector`** (`/amazon_sp`). The only Amazon operation that returns
51
+ real Amazon data. The SDK previously exposed ten Amazon operations that the proxy silently
52
+ rewrites to Windsor aggregates, and not this one.
53
+
54
+ ### Fixed — 23 Acumatica helpers built a malformed URL and 404'd on every call
55
+
56
+ `analyzePathPattern` converts a regex `pathPattern` into a template literal. A trailing bare
57
+ character class with no quantifier — Acumatica's `[^?/]` — matched none of its substitutions,
58
+ so `split("/")` sheared the class and the emitted path ended in the literal text `[^`.
59
+
60
+ ```
61
+ 2.2.1 /entity/Default/${defaultId}/SalesOrder/[^
62
+ 3.0.0 /entity/Default/${endpointVersion}/SalesOrder/${salesOrderId}
63
+ ```
64
+
65
+ This shipped: `npm unpack @on-belay/sdk@2.2.1` shows the leaked `[^` in `dist/index.js`.
66
+ Affected every detail fetch/update/delete across `SalesOrder`, `Customer`, `Vendor`,
67
+ `StockItem`, `NonStockItem`, `PurchaseOrder`, `Invoice`, `Payment`, `Contact`, `Employee`,
68
+ `GeneralLedgerTransaction`, `Account`, `Bill`.
69
+
70
+ **Each of these helpers now takes one more argument** — the resource key that used to be
71
+ `[^`. That is the breaking change, and it is the fix.
72
+
73
+ Also fixed: `sharepoint.searchDrive` emitted `/root/searchq='[^']*'` instead of
74
+ `/root/search(q='${param1}')`. New in this release, so nothing depended on the broken form.
75
+
76
+ ### Fixed — 14 Google helpers dropped the `:action` suffix from the path
77
+
78
+ `ga4.run_report.*`, `gemini.generateContent`, `gemini.streamGenerateContent`,
79
+ `gemini.embedContent`, `gemini.countTokens` and others emitted the resource path with the
80
+ action suffix **missing**:
81
+
82
+ ```
83
+ 2.2.1 /v1beta/properties/${propertyId}
84
+ 3.0.0 /v1beta/properties/${propertyId}:runReport
85
+ ```
86
+
87
+ A path segment that merely *contained* the parameter placeholder was being collapsed down
88
+ to the placeholder, so anything else in that segment was thrown away. Unlike the Acumatica
89
+ case this leaves nothing regex-shaped behind, so it is invisible to a "did any regex
90
+ punctuation survive?" check — the path is simply, silently, a different endpoint. Our own
91
+ proxy rejects it too, because the proxy validates the request path against the same
92
+ `pathPattern`.
93
+
94
+ The same collapse was dropping the literal `v` from version segments, so 27 Google Ads and
95
+ Salesforce helpers emitted `/${version}/…` where the API needs `/v18/…`. Now
96
+ `/v${version}/…`.
97
+
98
+ ### Fixed — a regex quantifier ate a parameter
99
+
100
+ `judgeMe.getProduct` emitted the literal path `/products/-`. Its pattern is
101
+ `^/products/-?\d+`; the parameter was substituted, then the query-string strip cut from the
102
+ `-?` quantifier onward and took the parameter with it. Now `/products/${productId}` — and
103
+ the helper takes the `productId` argument it always should have.
104
+
105
+ ### Fixed — 10 helpers were unparseable TypeScript
106
+
107
+ 44 catalog keys are dotted (`shopify.themes.list`, `ga4.run_report.overall`). The generator
108
+ left the dots in the function name, emitting `export async function shopify.themes.list<T>(`.
109
+ Ten such keys were already reachable, so the generator produced a file that would not parse.
110
+ Identifiers are now sanitized (`shopifyThemesList`); the dotted string is unchanged as the
111
+ operation key sent to the proxy.
112
+
113
+ ### Not changed — Shopify REST paths still omit `.json`
114
+
115
+ Unchanged from 2.2.1. Recorded because it was changed and reverted twice during review, and
116
+ the reasoning is worth having in one place.
117
+
118
+ ```
119
+ 2.2.1 and 3.0.0 /admin/api/{version}/orders
120
+ catalog pattern ^/admin/api/[^/]+/orders\.json ← requires .json
121
+ ```
122
+
123
+ **Your calls are not failing.** Shopify accepts both forms — 154 json-less `/admin/api/`
124
+ calls over 30 days, **every one status 200**, zero failures. And the platform check that
125
+ *would* reject a json-less path is unreachable from a fieldset: it runs only for
126
+ group-access callers with an enforcing grant, and fieldset tokens never take that branch.
127
+
128
+ ⚠️ **The argument for the other choice, stated fairly:** all 17 of the catalog's
129
+ hand-curated Shopify `/admin/api/` `examplePath` values carry `.json`. That is real, and it
130
+ is why this flipped once. It was judged **documentation** divergence rather than evidence of
131
+ breakage — a 55-path behaviour change needs a functional reason, and there is none for this
132
+ package's consumers. Those 17 appear in `PATH-DIVERGENCE.json` as expected entries.
133
+
134
+ ### Changed — caller-supplies-the-path operations now take that path as a parameter
135
+
136
+ Three patterns are bare prefixes rather than fixed endpoints. `shopify.admin.fetch` says so
137
+ in its own description — *"The path is chosen by the caller, so this operation is broad"* —
138
+ and its example is `/admin/api/2026-01/shop.json`. The generator emitted the prefix with no
139
+ parameter, so the helper could not reach any resource:
140
+
141
+ ```
142
+ 2.2.1 shopifyAdminFetch(orgId, slug, options) → /admin/api/2024-01
143
+ 3.0.0 shopifyAdminFetch(orgId, slug, resourcePath, …) → /admin/api/2026-01/${resourcePath}
144
+ ```
145
+
146
+ 5 operations: `shopify.admin.fetch`, github `get_file_contents` / `create_update_file`,
147
+ cloudinary `get_asset` / `delete_asset`. All five are new in 3.0.0 except
148
+ `shopify.admin.fetch`, whose 2.2.1 form could not be used for anything.
149
+
150
+ ### Known — 5 operations emit a path their pattern rejects, and are still shipped
151
+
152
+ `gmail.searchMessages`, `youtube.getVideo`, `googleDrive.downloadFile`,
153
+ `microsoftTeams.listTeams`, `yotpo.getProductReviews`. Their patterns bake a
154
+ **caller-supplied query value** into the path (`\?q=`, `\?.*id=`, `\?alt=media`, `\?.*resourceProvisioningOptions`), which a path
155
+ template cannot express.
156
+
157
+ **They are shipped, deliberately.** `youtube.getVideo` emits `/youtube/v3/videos` and that
158
+ exact path has **3 live 200s** in the last 90 days — pass the id in `queryParams`. An earlier
159
+ draft of this release omitted several of these; that was reverted. The patterns are
160
+ over-specified, and loosening them is a catalog change tracked separately.
161
+
162
+ All divergences are listed in `src/integrations/PATH-DIVERGENCE.json`.
163
+
164
+ ### Fixed — Shopify Admin API version was `2024-01`
165
+
166
+ Over a year past Shopify's end-of-life for that version. Now `2026-01`, read from the
167
+ platform's own `SHOPIFY_API_VERSION` constant at generate time so the two cannot drift.
168
+
169
+ ### Breaking changes — complete list
170
+
171
+ **27 changes require a caller to change code. 24 of them are path fixes.**
172
+
173
+ | Change | Count | Source |
174
+ |---|---:|---|
175
+ | Acumatica detail helpers take one more argument | 22 | **This release** — the 404 fix |
176
+ | `github.getFileContents` / `createUpdateFile` take a `resourcePath` argument | 2 | **This release** — the bare-prefix fix |
177
+ | `shopify.getAnalyticsReport`: `options.queryParams` → `options.body` | 1 | Catalog |
178
+ | `shopmonkey.getOrderFees` removed | 1 | Catalog |
179
+ | `shopmonkey.listInventory` removed | 1 | Catalog |
180
+
181
+ The last three are **catalog-driven, not authored in the SDK** — upstream operation
182
+ definitions changed and the SDK had simply not been regenerated since. `getAnalyticsReport`
183
+ moved because Shopify **deleted** the REST `reports.json` resource, so the operation is a
184
+ GraphQL POST now. Both `shopmonkey` keys were deleted from the catalog because the endpoints
185
+ no longer exist; `/v3/order/{id}/fee` 404s.
186
+
187
+ **41 parameters were renamed. These are not breaking for positional calls** — arity and
188
+ types are unchanged, and every helper is called positionally. They are corrections to names
189
+ a developer would otherwise trust and get wrong:
190
+
191
+ - `defaultId` → `endpointVersion` (Acumatica). The segment after `/entity/Default/` is the
192
+ endpoint *version* — `Default` is the endpoint *name* — so the old name read as an
193
+ identifier of something called Default.
194
+ - `salesorderId` → `salesOrderId`, `inventoryitemId` → `inventoryItemId`,
195
+ `generalledgertransactionId` → `generalLedgerTransactionId`, `localpostId` → `localPostId`,
196
+ and similar across `acumatica`, `netsuite`, `linkedin-ads`, `gooddata`,
197
+ `google-business-profile`. Names were being derived from a lowercased string, collapsing
198
+ the word boundaries.
199
+ - `accountId` → `adAccountId` on six `redditAds` helpers (catalog-driven).
200
+
201
+ ### Known affected consumers: none
202
+
203
+ `owl-shopmonkey-import` is the only external consumer. It pins `^2.2.0`, so it will **not**
204
+ pick up 3.0.0 automatically, and it calls none of the removed or changed helpers — verified
205
+ against its source: it uses `shopify.listInventoryLevels` and raw `executeProxyCall`.
206
+
207
+ ### Guardrails added, so this cannot recur silently
208
+
209
+ - The generator reads `prisma/operation-defs.ts` — the same aggregate seeded into the live
210
+ `IntegrationOperation` table — and **fails loudly** if any `operation-defs-part*.ts` file
211
+ on disk holds operations the aggregate does not carry. A future `part16` that nobody wires
212
+ in aborts the run and names the missing keys.
213
+ - **A path-divergence report.** After conversion, a value is substituted for every
214
+ `${param}` and the concrete path is tested against the source `pathPattern`. Mismatches
215
+ are written to a committed `src/integrations/PATH-DIVERGENCE.json` and the set is **pinned
216
+ by the test suite**, so it cannot grow unnoticed. Current run: **1,547 of 1,547 emitted,
217
+ 0 omitted, 64 divergences recorded.**
218
+ 🚫 It is deliberately **not** a gate. It was one, briefly, and as a gate it removed 55
219
+ working Shopify helpers plus `youtube.getVideo` — whose "broken" path has three live
220
+ 200s. A divergence says the template and the pattern disagree; it does not say which is
221
+ wrong.
222
+ - Colliding helper names, module filenames, or barrel namespaces abort the run instead of
223
+ silently shadowing.
224
+ - CI asserts the generated output matches the catalog exactly, in both directions, plus
225
+ identifier validity, barrel uniqueness, regression pins on each non-trivial path idiom,
226
+ and the Shopify version mirror. The staleness that hid these operations for three months
227
+ is now a red build, not a silence.
228
+ - ⚠️ **The divergence check replaced an earlier "no regex punctuation survived" check,
229
+ which was blind in both directions** and is the reason three defect classes in this
230
+ release shipped in 2.2.1 at all. A *dropped* literal leaves nothing regex-shaped behind
231
+ (`:runReport` vanishing passed it cleanly), and `?` is both regex punctuation and an
232
+ ordinary URL character (so google_drive's correct path was refused). The round-trip
233
+ subsumes it and needs no list of known idioms, so a shape nobody has seen yet is still
234
+ caught.
235
+ - **The examplePath check is the primary path signal.** 616 ops carry a hand-curated
236
+ `examplePath`; the test asserts every emitted template can produce its own. It caught what
237
+ nothing else could — `shopify.admin.fetch`'s bare prefix passed the pattern round-trip,
238
+ because a prefix pattern matches trivially. Three blind spots in sequence: punctuation
239
+ missed deletions, the round-trip missed under-specification, the example caught it.
240
+ **17 known mismatches**, pinned: 15 shopify + 1 yotpo are the deliberate json-less form
241
+ above, and 1 is `cloudinary.uploadAssetRemote`, where the pattern is a 4-way alternation
242
+ (`^/(image|video|auto|raw)/upload`) collapsed to the first option.
243
+ - Real path defects are pinned by tests that encode what the **upstream API** requires,
244
+ not agreement with a regex: the `:action` suffix and `v` version literal (28,142 live
245
+ google_ads calls, 100% carrying both), acumatica's sheared character class, and
246
+ 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 },