@open-neko/plugin-shopify 0.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/LICENSE +202 -0
- package/README.md +55 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +160 -0
- package/dist/plugin.js.map +1 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +15215 -0
- package/dist/run.js.map +1 -0
- package/dist/shopify-client.d.ts +98 -0
- package/dist/shopify-client.d.ts.map +1 -0
- package/dist/shopify-client.js +113 -0
- package/dist/shopify-client.js.map +1 -0
- package/package.json +95 -0
- package/skill/SKILL.md +116 -0
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,mBAAmB,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP wrapper for Shopify's Admin REST API. All network calls
|
|
3
|
+
* go through `fetchImpl` for testability — the production runtime
|
|
4
|
+
* uses globalThis.fetch; tests inject a stub.
|
|
5
|
+
*
|
|
6
|
+
* Shopify's API is per-store; the storeDomain in the config is the
|
|
7
|
+
* canonical *.myshopify.com host (NOT the custom storefront domain
|
|
8
|
+
* the operator may have configured for shoppers).
|
|
9
|
+
*
|
|
10
|
+
* Pinned API version: Shopify rotates quarterly. We default to
|
|
11
|
+
* "2026-01" — operators can override via SHOPIFY_API_VERSION when
|
|
12
|
+
* Shopify deprecates that version.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_API_VERSION = "2026-01";
|
|
15
|
+
export interface ShopifyClientConfig {
|
|
16
|
+
/** Canonical *.myshopify.com domain — no scheme. */
|
|
17
|
+
storeDomain: string;
|
|
18
|
+
accessToken: string;
|
|
19
|
+
apiVersion?: string;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
export interface ShopifyOrderSummary {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
order_number: number;
|
|
26
|
+
email: string | null;
|
|
27
|
+
financial_status: string | null;
|
|
28
|
+
fulfillment_status: string | null;
|
|
29
|
+
total_price: string;
|
|
30
|
+
currency: string;
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
tags: string;
|
|
34
|
+
note: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface ShopifyOrderDetail extends ShopifyOrderSummary {
|
|
37
|
+
customer: {
|
|
38
|
+
id: number;
|
|
39
|
+
email: string | null;
|
|
40
|
+
first_name: string | null;
|
|
41
|
+
last_name: string | null;
|
|
42
|
+
} | null;
|
|
43
|
+
line_items: Array<{
|
|
44
|
+
id: number;
|
|
45
|
+
title: string;
|
|
46
|
+
quantity: number;
|
|
47
|
+
sku: string | null;
|
|
48
|
+
price: string;
|
|
49
|
+
}>;
|
|
50
|
+
shipping_address: Record<string, unknown> | null;
|
|
51
|
+
billing_address: Record<string, unknown> | null;
|
|
52
|
+
refunds: Array<Record<string, unknown>>;
|
|
53
|
+
}
|
|
54
|
+
export interface ListOrdersParams {
|
|
55
|
+
/** ISO timestamp lower bound on created_at. */
|
|
56
|
+
createdAtMin?: string;
|
|
57
|
+
/** ISO timestamp upper bound on created_at. */
|
|
58
|
+
createdAtMax?: string;
|
|
59
|
+
/** "any" | "authorized" | "pending" | "paid" | "partially_paid" | "refunded" | "voided" | "partially_refunded" | "unpaid" */
|
|
60
|
+
financialStatus?: string;
|
|
61
|
+
/** "shipped" | "partial" | "unshipped" | "any" | "unfulfilled" */
|
|
62
|
+
fulfillmentStatus?: string;
|
|
63
|
+
/** 1-250 (Shopify max). Default 50. */
|
|
64
|
+
limit?: number;
|
|
65
|
+
/** "open" | "closed" | "cancelled" | "any". Default "open". */
|
|
66
|
+
status?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface UpdateOrderNoteParams {
|
|
69
|
+
orderId: number;
|
|
70
|
+
note: string;
|
|
71
|
+
/** When true, append " — <note>" to the existing note instead of replacing. */
|
|
72
|
+
append?: boolean;
|
|
73
|
+
/** Optional tags to add (comma-joined). */
|
|
74
|
+
addTags?: string[];
|
|
75
|
+
}
|
|
76
|
+
export declare class ShopifyClient {
|
|
77
|
+
private readonly cfg;
|
|
78
|
+
private readonly fetchImpl;
|
|
79
|
+
private readonly apiVersion;
|
|
80
|
+
constructor(cfg: ShopifyClientConfig);
|
|
81
|
+
private url;
|
|
82
|
+
private headers;
|
|
83
|
+
listOrders(params?: ListOrdersParams): Promise<{
|
|
84
|
+
orders: ShopifyOrderSummary[];
|
|
85
|
+
}>;
|
|
86
|
+
getOrder(orderId: number): Promise<{
|
|
87
|
+
order: ShopifyOrderDetail;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Append-or-replace the note + optionally add tags. Shopify's PUT
|
|
91
|
+
* order endpoint is field-by-field PATCH semantics: only the keys
|
|
92
|
+
* you pass change.
|
|
93
|
+
*/
|
|
94
|
+
updateOrderNote(params: UpdateOrderNoteParams): Promise<{
|
|
95
|
+
order: ShopifyOrderDetail;
|
|
96
|
+
}>;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=shopify-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shopify-client.d.ts","sourceRoot":"","sources":["../src/shopify-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP wrapper for Shopify's Admin REST API. All network calls
|
|
3
|
+
* go through `fetchImpl` for testability — the production runtime
|
|
4
|
+
* uses globalThis.fetch; tests inject a stub.
|
|
5
|
+
*
|
|
6
|
+
* Shopify's API is per-store; the storeDomain in the config is the
|
|
7
|
+
* canonical *.myshopify.com host (NOT the custom storefront domain
|
|
8
|
+
* the operator may have configured for shoppers).
|
|
9
|
+
*
|
|
10
|
+
* Pinned API version: Shopify rotates quarterly. We default to
|
|
11
|
+
* "2026-01" — operators can override via SHOPIFY_API_VERSION when
|
|
12
|
+
* Shopify deprecates that version.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_API_VERSION = "2026-01";
|
|
15
|
+
export class ShopifyClient {
|
|
16
|
+
cfg;
|
|
17
|
+
fetchImpl;
|
|
18
|
+
apiVersion;
|
|
19
|
+
constructor(cfg) {
|
|
20
|
+
this.cfg = cfg;
|
|
21
|
+
this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
|
|
22
|
+
this.apiVersion = cfg.apiVersion ?? DEFAULT_API_VERSION;
|
|
23
|
+
}
|
|
24
|
+
url(path) {
|
|
25
|
+
return `https://${this.cfg.storeDomain}/admin/api/${this.apiVersion}/${path}`;
|
|
26
|
+
}
|
|
27
|
+
headers(extra = {}) {
|
|
28
|
+
return {
|
|
29
|
+
"X-Shopify-Access-Token": this.cfg.accessToken,
|
|
30
|
+
Accept: "application/json",
|
|
31
|
+
...extra,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async listOrders(params = {}) {
|
|
35
|
+
const u = new URL(this.url("orders.json"));
|
|
36
|
+
u.searchParams.set("limit", String(clampInt(params.limit ?? 50, 1, 250)));
|
|
37
|
+
u.searchParams.set("status", params.status ?? "open");
|
|
38
|
+
if (params.financialStatus)
|
|
39
|
+
u.searchParams.set("financial_status", params.financialStatus);
|
|
40
|
+
if (params.fulfillmentStatus)
|
|
41
|
+
u.searchParams.set("fulfillment_status", params.fulfillmentStatus);
|
|
42
|
+
if (params.createdAtMin)
|
|
43
|
+
u.searchParams.set("created_at_min", params.createdAtMin);
|
|
44
|
+
if (params.createdAtMax)
|
|
45
|
+
u.searchParams.set("created_at_max", params.createdAtMax);
|
|
46
|
+
const res = await this.fetchImpl(u.toString(), { headers: this.headers() });
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
throw await shopifyError(res, "list_shopify_orders");
|
|
49
|
+
return (await res.json());
|
|
50
|
+
}
|
|
51
|
+
async getOrder(orderId) {
|
|
52
|
+
const res = await this.fetchImpl(this.url(`orders/${orderId}.json`), {
|
|
53
|
+
headers: this.headers(),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
throw await shopifyError(res, "get_shopify_order");
|
|
57
|
+
return (await res.json());
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Append-or-replace the note + optionally add tags. Shopify's PUT
|
|
61
|
+
* order endpoint is field-by-field PATCH semantics: only the keys
|
|
62
|
+
* you pass change.
|
|
63
|
+
*/
|
|
64
|
+
async updateOrderNote(params) {
|
|
65
|
+
let nextNote = params.note;
|
|
66
|
+
let nextTags;
|
|
67
|
+
if (params.append || params.addTags?.length) {
|
|
68
|
+
// Need the current state to append safely. Fetch once.
|
|
69
|
+
const current = await this.getOrder(params.orderId);
|
|
70
|
+
if (params.append) {
|
|
71
|
+
const existing = current.order.note ?? "";
|
|
72
|
+
nextNote = existing ? `${existing} — ${params.note}` : params.note;
|
|
73
|
+
}
|
|
74
|
+
if (params.addTags?.length) {
|
|
75
|
+
const existingTags = (current.order.tags ?? "")
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((s) => s.trim())
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
const merged = new Set(existingTags);
|
|
80
|
+
for (const t of params.addTags)
|
|
81
|
+
merged.add(t);
|
|
82
|
+
nextTags = [...merged].join(", ");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const body = { id: params.orderId, note: nextNote };
|
|
86
|
+
if (nextTags !== undefined)
|
|
87
|
+
body.tags = nextTags;
|
|
88
|
+
const res = await this.fetchImpl(this.url(`orders/${params.orderId}.json`), {
|
|
89
|
+
method: "PUT",
|
|
90
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
91
|
+
body: JSON.stringify({ order: body }),
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok)
|
|
94
|
+
throw await shopifyError(res, "update_shopify_order_note");
|
|
95
|
+
return (await res.json());
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function clampInt(value, min, max) {
|
|
99
|
+
if (!Number.isFinite(value))
|
|
100
|
+
return min;
|
|
101
|
+
return Math.min(Math.max(Math.trunc(value), min), max);
|
|
102
|
+
}
|
|
103
|
+
async function shopifyError(res, action) {
|
|
104
|
+
let body = "";
|
|
105
|
+
try {
|
|
106
|
+
body = await res.text();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* ignore */
|
|
110
|
+
}
|
|
111
|
+
return new Error(`${action} failed (HTTP ${res.status}): ${body || res.statusText}`);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=shopify-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shopify-client.js","sourceRoot":"","sources":["../src/shopify-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-neko/plugin-shopify",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shopify Admin API connector for OpenNeko — list orders, fetch order detail, and update internal order notes against an operator's store. Three sandboxed actions: list_shopify_orders, get_shopify_order, update_shopify_order_note. Network egress limited to the operator's *.myshopify.com host.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/open-neko/plugins.git",
|
|
9
|
+
"directory": "packages/shopify"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"skill"
|
|
16
|
+
],
|
|
17
|
+
"openneko": {
|
|
18
|
+
"runner": "./dist/run.js",
|
|
19
|
+
"skill": "./skill",
|
|
20
|
+
"permissions": {
|
|
21
|
+
"network": [
|
|
22
|
+
"*.myshopify.com"
|
|
23
|
+
],
|
|
24
|
+
"env": [
|
|
25
|
+
{
|
|
26
|
+
"key": "SHOPIFY_STORE_DOMAIN",
|
|
27
|
+
"required": true,
|
|
28
|
+
"secret": false,
|
|
29
|
+
"description": "Your store's permanent myshopify.com subdomain — e.g. acme.myshopify.com. Use the .myshopify.com domain even if you've configured a custom storefront domain; the Admin API only honors the canonical one."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"key": "SHOPIFY_ACCESS_TOKEN",
|
|
33
|
+
"required": true,
|
|
34
|
+
"secret": true,
|
|
35
|
+
"description": "Shopify Admin API access token (starts with shpat_). Create at admin → Settings → Apps and sales channels → Develop apps → Create an app → Configuration → Admin API access scopes. Needs read_orders + write_orders. Token is shown ONCE on install — copy it immediately."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"key": "SHOPIFY_API_VERSION",
|
|
39
|
+
"required": false,
|
|
40
|
+
"secret": false,
|
|
41
|
+
"description": "Shopify Admin API version to pin (defaults to 2026-01). Shopify rotates quarterly; pin a stable version to avoid surprise breakage."
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"capabilities": {
|
|
46
|
+
"action": {
|
|
47
|
+
"kinds": [
|
|
48
|
+
{
|
|
49
|
+
"kind": "list_shopify_orders",
|
|
50
|
+
"description": "List recent orders, optionally filtered by financialStatus, fulfillmentStatus, or a date range (createdAtMin/createdAtMax).",
|
|
51
|
+
"default_mode": "auto",
|
|
52
|
+
"example": {
|
|
53
|
+
"financialStatus": "paid",
|
|
54
|
+
"createdAtMin": "2026-05-01T00:00:00Z",
|
|
55
|
+
"limit": 20
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"kind": "get_shopify_order",
|
|
60
|
+
"description": "Fetch full detail for one order by id — line items, customer, addresses, refunds, tags.",
|
|
61
|
+
"default_mode": "auto",
|
|
62
|
+
"example": {
|
|
63
|
+
"orderId": 1234567890
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"kind": "update_shopify_order_note",
|
|
68
|
+
"description": "Set or append the internal note on an order (user-visible in Shopify admin only; not customer-facing).",
|
|
69
|
+
"default_mode": "ask",
|
|
70
|
+
"example": {
|
|
71
|
+
"orderId": 1234567890,
|
|
72
|
+
"note": "Flagged for review by ops.",
|
|
73
|
+
"append": true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"@open-neko/plugin-types": "0.1.0"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@types/node": "^20.19.0",
|
|
85
|
+
"esbuild": "^0.25.0",
|
|
86
|
+
"typescript": "^5.6.3",
|
|
87
|
+
"vitest": "^2.1.8"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "tsc -p tsconfig.json && node scripts/bundle.mjs",
|
|
91
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
92
|
+
"test": "vitest run",
|
|
93
|
+
"test:watch": "vitest"
|
|
94
|
+
}
|
|
95
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shopify
|
|
3
|
+
description: Patterns for the @open-neko/plugin-shopify actions — when to use list_shopify_orders vs get_shopify_order, how to filter for a specific operations question, and how to log internal context via update_shopify_order_note. Use whenever the operator's question is about orders flowing through their Shopify store (revenue checks, fulfillment status, customer service follow-ups, ops tagging). Assumes SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN are configured.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
authoredBy: open-neko
|
|
7
|
+
pairsWith: "@open-neko/plugin-shopify"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Shopify patterns
|
|
11
|
+
|
|
12
|
+
Shopify is OpenNeko's reference ecommerce connector. Most operator
|
|
13
|
+
questions fall into one of three shapes:
|
|
14
|
+
|
|
15
|
+
| Operator says | Right action | Why |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| "What's happened in the last hour?" | `list_shopify_orders` with `createdAtMin` set to T-1h | Bounded read, server-side filter |
|
|
18
|
+
| "Pull up order #1234" | `get_shopify_order` with the parsed number | Single-record fetch, full detail |
|
|
19
|
+
| "Mark this order as 'priority shipping' and tell ops we noticed" | `update_shopify_order_note` with `append: true` and `addTags: ["priority"]` | One write, preserves prior state |
|
|
20
|
+
|
|
21
|
+
## Identifying an order from operator input
|
|
22
|
+
|
|
23
|
+
Operators usually paste either:
|
|
24
|
+
|
|
25
|
+
- `#1234` — Shopify's display name. The numeric part after `#` is
|
|
26
|
+
`order_number`, NOT `id`. The plugin's actions need `id` (the
|
|
27
|
+
internal numeric identifier).
|
|
28
|
+
- `https://acme.myshopify.com/admin/orders/4567890123456` — the
|
|
29
|
+
number in the URL IS `id`. Use it directly.
|
|
30
|
+
- A bare integer the operator says is "order 4567890123456" — same.
|
|
31
|
+
|
|
32
|
+
If the operator gives you `#1234`, run a `list_shopify_orders` with
|
|
33
|
+
no filter and `limit: 50`, then match against `order_number`. Don't
|
|
34
|
+
guess.
|
|
35
|
+
|
|
36
|
+
## `list_shopify_orders` patterns
|
|
37
|
+
|
|
38
|
+
### Filter server-side, always
|
|
39
|
+
|
|
40
|
+
Default `status: "open"` keeps you off completed/archived orders.
|
|
41
|
+
Override only when the operator's question explicitly includes
|
|
42
|
+
them ("how many cancellations did we get yesterday?" → `status:
|
|
43
|
+
"cancelled"`).
|
|
44
|
+
|
|
45
|
+
Three commonly useful filter combos:
|
|
46
|
+
|
|
47
|
+
- **"What's stuck?"** → `fulfillmentStatus: "unfulfilled"`, `financialStatus: "paid"`
|
|
48
|
+
- **"What needs payment chasing?"** → `financialStatus: "pending"`
|
|
49
|
+
- **"What landed today?"** → `createdAtMin` set to today's midnight in the store's timezone
|
|
50
|
+
|
|
51
|
+
### Page size
|
|
52
|
+
|
|
53
|
+
Default 50; cap at 250 (Shopify's max per call). Bigger pages are
|
|
54
|
+
slower and ALL of the payload comes through the agent's context. For
|
|
55
|
+
"show me the last 10" requests, set `limit: 10`.
|
|
56
|
+
|
|
57
|
+
### Time zones matter
|
|
58
|
+
|
|
59
|
+
Shopify returns ISO timestamps in UTC. The operator usually thinks
|
|
60
|
+
in their own timezone. If the question is "what happened today?",
|
|
61
|
+
the agent should convert the operator's local "today" to a UTC
|
|
62
|
+
ISO range before passing to `createdAtMin`/`createdAtMax`. Don't
|
|
63
|
+
ask Shopify to filter on a calendar day in the wrong timezone.
|
|
64
|
+
|
|
65
|
+
## `update_shopify_order_note` patterns
|
|
66
|
+
|
|
67
|
+
This is the only write action in this plugin, and it's deliberately
|
|
68
|
+
low-blast-radius: the `note` field is operator-visible inside the
|
|
69
|
+
Shopify admin only, never customer-facing.
|
|
70
|
+
|
|
71
|
+
### Always prefer `append: true` for ops logging
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
note: "OpenNeko revenue-drop watcher pinged @amit at 14:03"
|
|
75
|
+
append: true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Operators rely on the note as a running log. Replacing it wipes
|
|
79
|
+
their history. The only time to omit `append` is when the operator
|
|
80
|
+
explicitly says "replace the note with…".
|
|
81
|
+
|
|
82
|
+
### Pair with tags for filterable ops state
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
addTags: ["needs-review", "noticed-by-openneko"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Tags ARE searchable in Shopify admin's order list, notes aren't.
|
|
89
|
+
Adding a tag like `"noticed-by-openneko"` lets the operator pull
|
|
90
|
+
"show me everything OpenNeko flagged this week" later.
|
|
91
|
+
|
|
92
|
+
## Failure modes the operator should see
|
|
93
|
+
|
|
94
|
+
- **`401 Unauthorized`** — `SHOPIFY_ACCESS_TOKEN` expired or scope
|
|
95
|
+
insufficient. Prompt the operator to regenerate the token with
|
|
96
|
+
read_orders + write_orders.
|
|
97
|
+
- **`404 Not Found`** on an order id — usually the operator gave you
|
|
98
|
+
the `order_number` (#1234) instead of the `id`. Re-run `list` to
|
|
99
|
+
resolve.
|
|
100
|
+
- **`429 Too Many Requests`** — Shopify rate limits per app per
|
|
101
|
+
store (2 req/sec leaky bucket on REST). Back off; if the operator
|
|
102
|
+
is waiting, surface the Retry-After header so they know how long.
|
|
103
|
+
- **`Invalid SHOPIFY_STORE_DOMAIN`** — the plugin rejects domains
|
|
104
|
+
that aren't `*.myshopify.com`. Custom storefront domains don't
|
|
105
|
+
work on the Admin API.
|
|
106
|
+
|
|
107
|
+
## What this skill is NOT for
|
|
108
|
+
|
|
109
|
+
- Storefront product browsing — that's the public Storefront API,
|
|
110
|
+
a different scope set, and this connector doesn't enable it.
|
|
111
|
+
- Customer-facing email — Shopify can send transactional emails;
|
|
112
|
+
this connector doesn't trigger them. Use the operator's
|
|
113
|
+
email/Gmail connector for that.
|
|
114
|
+
- Bulk migrations — Shopify's REST API caps you at ~2 req/sec.
|
|
115
|
+
For >1000 changes, point the operator at Shopify's bulk operations
|
|
116
|
+
GraphQL endpoint, not this plugin.
|