From e240f3affbd89004c6f39d8413b8bdef9622b96c Mon Sep 17 00:00:00 2001 From: Saim Shafique Date: Fri, 30 Oct 2026 20:20:03 +0500 Subject: [PATCH] fix(plugins): probe a store connection against the store itself probeActionFor prefers an identity read matching IDENTITY_ACTION, else the first safe read alphabetically. For Shopify that was SHOPIFY_COUNT_BLOGS, which needs the read_content scope a store app with only order and product scopes lacks, so a working key was reported as rejected. A store's own details are the identity: _GET_SHOP_DETAILS now counts. Co-authored-by: Saim Shafique <128062316+sx4im@users.noreply.github.com> --- CHANGELOG.md | 8 +++++ server/src/plugins/plugin-store/model.ts | 2 +- .../plugin-store-identity-action.test.ts | 34 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 server/tests/plugin-store-identity-action.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9d3f16f4..fed04894c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,14 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. `COMPUTER_MEMORY_HIGH_PERCENT=0` and `COMPUTER_TAB_IDLE_MS=0` (Helm: `computers.memoryHighPercent` and `computers.tabIdleMs`) keep the old behaviour. +### A store connection is checked against the store itself + +After connecting a key, OpenBot checks it with one argument-free read, preferring an identity read +and otherwise taking the first safe read alphabetically. For Shopify that was `SHOPIFY_COUNT_BLOGS`, +which needs the `read_content` scope a store app with only order and product scopes does not have, +so a working key was reported as rejected. A store's own details now count as an identity read, so +the check calls `SHOPIFY_GET_SHOP_DETAILS` instead. + ### A computer closes browsers nobody is using when its memory runs high A computer runs one browser for each Bot that is browsing, and the only limits were a count and half diff --git a/server/src/plugins/plugin-store/model.ts b/server/src/plugins/plugin-store/model.ts index 1231729e7d..191c1f2fff 100644 --- a/server/src/plugins/plugin-store/model.ts +++ b/server/src/plugins/plugin-store/model.ts @@ -1114,7 +1114,7 @@ export const CLIENT_REREGISTRATION_BACKOFF_MS = 5 * 60_000; * `LINEAR_GET_ME` — and an unanchored match would take `SLACK_PROFILE_SET` for an identity read. */ export const IDENTITY_ACTION = - /(_GET_ME|_PROFILE|_CURRENT_USER|_USER_INFO|_WHOAMI)$/; + /(_GET_ME|_PROFILE|_CURRENT_USER|_USER_INFO|_WHOAMI|_GET_SHOP_DETAILS)$/; /** * Every broker's id namespace, reserved together. See {@link BROKERED_PROFILE} in `./access` for diff --git a/server/tests/plugin-store-identity-action.test.ts b/server/tests/plugin-store-identity-action.test.ts new file mode 100644 index 0000000000..1cb064d582 --- /dev/null +++ b/server/tests/plugin-store-identity-action.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test"; +import { IDENTITY_ACTION } from "../src/plugins/plugin-store/model"; + +describe("IDENTITY_ACTION", () => { + test("recognises a store's own details as an identity read", () => { + // For a store, its own details are the identity: probing these needs no + // extra scope, unlike e.g. SHOPIFY_COUNT_BLOGS which needs read_content + // and 403s on a store app with only order and product scopes. + expect(IDENTITY_ACTION.test("SHOPIFY_GET_SHOP_DETAILS")).toBe(true); + }); + + test("still recognises the existing identity reads", () => { + for (const name of [ + "STRIPE_GET_ME", + "SLACK_GET_PROFILE", + "LINEAR_GET_CURRENT_USER", + "GITHUB_GET_USER_INFO", + "NOTION_WHOAMI", + ]) { + expect(IDENTITY_ACTION.test(name)).toBe(true); + } + }); + + test("does not match non-identity actions", () => { + for (const name of [ + "SHOPIFY_COUNT_BLOGS", + "SHOPIFY_UPDATE_ORDER", + "SLACK_PROFILE_SET", + "GITHUB_WHOAMI_STATS", + ]) { + expect(IDENTITY_ACTION.test(name)).toBe(false); + } + }); +});