@sadrmanesh/commerce-agent-skills 0.0.0-stage → 0.3.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.
Files changed (201) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/bin/commerce-agent-skills.mjs +368 -0
  5. package/package.json +52 -4
  6. package/rules/b2b-pricing.md +11 -0
  7. package/rules/catalog-import-and-migration.md +11 -0
  8. package/rules/checkout-ux-and-conversion.md +12 -0
  9. package/rules/conversion-event-tracking.md +11 -0
  10. package/rules/ecommerce-seo-structured-data.md +11 -0
  11. package/rules/fraud-and-risk-basics.md +11 -0
  12. package/rules/google-merchant-product-feeds.md +11 -0
  13. package/rules/headless-nextjs-storefront.md +10 -0
  14. package/rules/inventory-and-multi-warehouse.md +10 -0
  15. package/rules/iranian-payment-gateways.md +10 -0
  16. package/rules/marketplace-search-and-ranking.md +10 -0
  17. package/rules/mcp-store-tooling.md +11 -0
  18. package/rules/multi-vendor-payouts.md +11 -0
  19. package/rules/order-and-webhook-reliability.md +12 -0
  20. package/rules/payment-gateway-integration.md +12 -0
  21. package/rules/product-catalog-modeling.md +11 -0
  22. package/rules/product-description-guardrails.md +10 -0
  23. package/rules/returns-and-refunds.md +10 -0
  24. package/rules/rtl-and-persian-localization.md +11 -0
  25. package/rules/shopify-app-development.md +10 -0
  26. package/rules/storefront-accessibility.md +11 -0
  27. package/rules/storefront-performance.md +10 -0
  28. package/rules/stripe-payments.md +10 -0
  29. package/rules/subscription-billing.md +10 -0
  30. package/rules/tax-and-shipping.md +10 -0
  31. package/rules/vector-product-search.md +10 -0
  32. package/rules/woocommerce-plugin-development.md +10 -0
  33. package/rules/woocommerce-rest-api.md +10 -0
  34. package/rules/wordpress-plugin-security.md +10 -0
  35. package/scripts/lib/frontmatter.mjs +218 -0
  36. package/skills/b2b-pricing/SKILL.md +55 -0
  37. package/skills/b2b-pricing/evals/examples/net-terms.md +3 -0
  38. package/skills/b2b-pricing/evals/examples/tiered-wholesale.md +3 -0
  39. package/skills/b2b-pricing/evals/tasks.json +19 -0
  40. package/skills/b2b-pricing/references/price-lists.md +46 -0
  41. package/skills/b2b-pricing/scripts/resolve-price.mjs +93 -0
  42. package/skills/catalog-import-and-migration/SKILL.md +58 -0
  43. package/skills/catalog-import-and-migration/evals/examples/platform-migration-urls.md +3 -0
  44. package/skills/catalog-import-and-migration/evals/examples/rerunnable-import.md +3 -0
  45. package/skills/catalog-import-and-migration/evals/tasks.json +20 -0
  46. package/skills/catalog-import-and-migration/references/field-mapping.md +43 -0
  47. package/skills/catalog-import-and-migration/scripts/check-import-rows.mjs +75 -0
  48. package/skills/catalog-order.txt +29 -0
  49. package/skills/checkout-ux-and-conversion/SKILL.md +86 -0
  50. package/skills/checkout-ux-and-conversion/evals/examples/guest-total.md +3 -0
  51. package/skills/checkout-ux-and-conversion/evals/examples/single-submit.md +3 -0
  52. package/skills/checkout-ux-and-conversion/evals/tasks.json +30 -0
  53. package/skills/checkout-ux-and-conversion/references/checkout-review.md +41 -0
  54. package/skills/conversion-event-tracking/SKILL.md +64 -0
  55. package/skills/conversion-event-tracking/evals/examples/purchase-once.md +3 -0
  56. package/skills/conversion-event-tracking/evals/examples/variant-item-id.md +3 -0
  57. package/skills/conversion-event-tracking/evals/tasks.json +30 -0
  58. package/skills/conversion-event-tracking/references/events.md +36 -0
  59. package/skills/conversion-event-tracking/scripts/check-event.mjs +79 -0
  60. package/skills/ecommerce-seo-structured-data/SKILL.md +84 -0
  61. package/skills/ecommerce-seo-structured-data/assets/example-product.jsonld +15 -0
  62. package/skills/ecommerce-seo-structured-data/evals/examples/extend-woocommerce-schema.md +8 -0
  63. package/skills/ecommerce-seo-structured-data/evals/examples/variant-offer.md +7 -0
  64. package/skills/ecommerce-seo-structured-data/evals/tasks.json +29 -0
  65. package/skills/ecommerce-seo-structured-data/references/product-jsonld.md +83 -0
  66. package/skills/ecommerce-seo-structured-data/scripts/check-product-jsonld.mjs +211 -0
  67. package/skills/fraud-and-risk-basics/SKILL.md +66 -0
  68. package/skills/fraud-and-risk-basics/evals/examples/review-queue.md +3 -0
  69. package/skills/fraud-and-risk-basics/evals/examples/wait-for-3ds.md +3 -0
  70. package/skills/fraud-and-risk-basics/evals/tasks.json +30 -0
  71. package/skills/google-merchant-product-feeds/SKILL.md +70 -0
  72. package/skills/google-merchant-product-feeds/assets/example-item.json +11 -0
  73. package/skills/google-merchant-product-feeds/evals/examples/generated-title.md +3 -0
  74. package/skills/google-merchant-product-feeds/evals/examples/stable-variant-id.md +3 -0
  75. package/skills/google-merchant-product-feeds/evals/tasks.json +30 -0
  76. package/skills/google-merchant-product-feeds/references/attributes.md +39 -0
  77. package/skills/google-merchant-product-feeds/scripts/check-feed-item.mjs +92 -0
  78. package/skills/headless-nextjs-storefront/SKILL.md +69 -0
  79. package/skills/headless-nextjs-storefront/evals/examples/revalidate-stock.md +7 -0
  80. package/skills/headless-nextjs-storefront/evals/examples/server-price.md +3 -0
  81. package/skills/headless-nextjs-storefront/evals/tasks.json +30 -0
  82. package/skills/headless-nextjs-storefront/references/cache-and-cart.md +39 -0
  83. package/skills/inventory-and-multi-warehouse/SKILL.md +61 -0
  84. package/skills/inventory-and-multi-warehouse/evals/examples/reserve-last-unit.md +3 -0
  85. package/skills/inventory-and-multi-warehouse/evals/examples/split-order.md +3 -0
  86. package/skills/inventory-and-multi-warehouse/evals/tasks.json +20 -0
  87. package/skills/inventory-and-multi-warehouse/references/platforms.md +40 -0
  88. package/skills/inventory-and-multi-warehouse/scripts/allocate-stock.mjs +72 -0
  89. package/skills/iranian-payment-gateways/SKILL.md +64 -0
  90. package/skills/iranian-payment-gateways/evals/examples/toman-store.md +3 -0
  91. package/skills/iranian-payment-gateways/evals/examples/zarinpal-callback.md +3 -0
  92. package/skills/iranian-payment-gateways/evals/tasks.json +20 -0
  93. package/skills/iranian-payment-gateways/references/zarinpal-v4.md +44 -0
  94. package/skills/iranian-payment-gateways/scripts/check-verify.mjs +62 -0
  95. package/skills/marketplace-search-and-ranking/SKILL.md +93 -0
  96. package/skills/marketplace-search-and-ranking/evals/examples/index-variant.md +3 -0
  97. package/skills/marketplace-search-and-ranking/evals/examples/normalize-query.md +7 -0
  98. package/skills/marketplace-search-and-ranking/evals/tasks.json +29 -0
  99. package/skills/marketplace-search-and-ranking/references/document-and-ranking.md +56 -0
  100. package/skills/marketplace-search-and-ranking/scripts/normalize-search-text.mjs +40 -0
  101. package/skills/mcp-store-tooling/SKILL.md +49 -0
  102. package/skills/mcp-store-tooling/evals/examples/refund-tool-design.md +3 -0
  103. package/skills/mcp-store-tooling/evals/examples/remote-server-auth.md +3 -0
  104. package/skills/mcp-store-tooling/evals/tasks.json +20 -0
  105. package/skills/mcp-store-tooling/references/mcp-notes.md +67 -0
  106. package/skills/mcp-store-tooling/scripts/check-tool-definitions.mjs +69 -0
  107. package/skills/multi-vendor-payouts/SKILL.md +71 -0
  108. package/skills/multi-vendor-payouts/evals/examples/retry-payout.md +3 -0
  109. package/skills/multi-vendor-payouts/evals/examples/split-paid-order.md +3 -0
  110. package/skills/multi-vendor-payouts/evals/tasks.json +30 -0
  111. package/skills/multi-vendor-payouts/references/ledger.md +35 -0
  112. package/skills/order-and-webhook-reliability/SKILL.md +99 -0
  113. package/skills/order-and-webhook-reliability/evals/examples/dedupe-resource-version.md +3 -0
  114. package/skills/order-and-webhook-reliability/evals/examples/verify-raw-body.md +3 -0
  115. package/skills/order-and-webhook-reliability/evals/tasks.json +29 -0
  116. package/skills/order-and-webhook-reliability/references/woocommerce-webhooks.md +58 -0
  117. package/skills/order-and-webhook-reliability/scripts/verify-hmac-signature.mjs +78 -0
  118. package/skills/payment-gateway-integration/SKILL.md +90 -0
  119. package/skills/payment-gateway-integration/evals/examples/confirmed-capture.md +9 -0
  120. package/skills/payment-gateway-integration/evals/examples/replayed-webhook.md +3 -0
  121. package/skills/payment-gateway-integration/evals/tasks.json +30 -0
  122. package/skills/payment-gateway-integration/references/woocommerce-gateway.md +67 -0
  123. package/skills/product-catalog-modeling/SKILL.md +91 -0
  124. package/skills/product-catalog-modeling/assets/example-catalog.json +45 -0
  125. package/skills/product-catalog-modeling/evals/examples/create-variation.md +10 -0
  126. package/skills/product-catalog-modeling/evals/examples/sync-parent.md +7 -0
  127. package/skills/product-catalog-modeling/evals/tasks.json +29 -0
  128. package/skills/product-catalog-modeling/references/variation-model.md +71 -0
  129. package/skills/product-catalog-modeling/scripts/validate-catalog.mjs +234 -0
  130. package/skills/product-description-guardrails/SKILL.md +84 -0
  131. package/skills/product-description-guardrails/evals/examples/facts-only.md +3 -0
  132. package/skills/product-description-guardrails/evals/examples/label-generated.md +3 -0
  133. package/skills/product-description-guardrails/evals/tasks.json +30 -0
  134. package/skills/product-description-guardrails/references/claims.md +24 -0
  135. package/skills/product-description-guardrails/scripts/check-description.mjs +61 -0
  136. package/skills/returns-and-refunds/SKILL.md +55 -0
  137. package/skills/returns-and-refunds/evals/examples/refund-after-inspection.md +3 -0
  138. package/skills/returns-and-refunds/evals/examples/woocommerce-partial-refund.md +3 -0
  139. package/skills/returns-and-refunds/evals/tasks.json +20 -0
  140. package/skills/returns-and-refunds/references/rma-states.md +37 -0
  141. package/skills/rtl-and-persian-localization/SKILL.md +81 -0
  142. package/skills/rtl-and-persian-localization/evals/examples/document-direction.md +7 -0
  143. package/skills/rtl-and-persian-localization/evals/examples/rial-display.md +7 -0
  144. package/skills/rtl-and-persian-localization/evals/tasks.json +30 -0
  145. package/skills/rtl-and-persian-localization/references/persian-text.md +51 -0
  146. package/skills/rtl-and-persian-localization/scripts/normalize-persian.mjs +62 -0
  147. package/skills/shopify-app-development/SKILL.md +73 -0
  148. package/skills/shopify-app-development/evals/examples/dedupe-delivery.md +3 -0
  149. package/skills/shopify-app-development/evals/examples/verify-webhook.md +3 -0
  150. package/skills/shopify-app-development/evals/tasks.json +29 -0
  151. package/skills/shopify-app-development/references/auth-and-webhooks.md +44 -0
  152. package/skills/storefront-accessibility/SKILL.md +59 -0
  153. package/skills/storefront-accessibility/evals/examples/add-to-cart-announcement.md +3 -0
  154. package/skills/storefront-accessibility/evals/examples/checkout-errors.md +3 -0
  155. package/skills/storefront-accessibility/evals/tasks.json +20 -0
  156. package/skills/storefront-accessibility/references/wcag-checklist.md +41 -0
  157. package/skills/storefront-performance/SKILL.md +59 -0
  158. package/skills/storefront-performance/evals/examples/cache-without-leaking-carts.md +3 -0
  159. package/skills/storefront-performance/evals/examples/slow-product-lcp.md +3 -0
  160. package/skills/storefront-performance/evals/tasks.json +20 -0
  161. package/skills/storefront-performance/references/web-vitals.md +55 -0
  162. package/skills/storefront-performance/scripts/check-web-vitals.mjs +49 -0
  163. package/skills/stripe-payments/SKILL.md +72 -0
  164. package/skills/stripe-payments/evals/examples/checkout-session-delayed.md +3 -0
  165. package/skills/stripe-payments/evals/examples/mark-paid-from-webhook.md +5 -0
  166. package/skills/stripe-payments/evals/examples/partial-refund.md +3 -0
  167. package/skills/stripe-payments/evals/tasks.json +27 -0
  168. package/skills/stripe-payments/references/stripe-objects.md +54 -0
  169. package/skills/stripe-payments/scripts/verify-stripe-signature.mjs +62 -0
  170. package/skills/subscription-billing/SKILL.md +76 -0
  171. package/skills/subscription-billing/evals/examples/cancel-at-period-end.md +3 -0
  172. package/skills/subscription-billing/evals/examples/one-renewal.md +3 -0
  173. package/skills/subscription-billing/evals/tasks.json +30 -0
  174. package/skills/subscription-billing/references/states.md +27 -0
  175. package/skills/tax-and-shipping/SKILL.md +61 -0
  176. package/skills/tax-and-shipping/evals/examples/store-tax-lines.md +3 -0
  177. package/skills/tax-and-shipping/evals/examples/unknown-zone.md +3 -0
  178. package/skills/tax-and-shipping/evals/tasks.json +30 -0
  179. package/skills/tax-and-shipping/references/woocommerce.md +22 -0
  180. package/skills/vector-product-search/SKILL.md +66 -0
  181. package/skills/vector-product-search/evals/examples/reembed-on-title.md +3 -0
  182. package/skills/vector-product-search/evals/examples/sku-before-vector.md +3 -0
  183. package/skills/vector-product-search/evals/tasks.json +30 -0
  184. package/skills/vector-product-search/references/index.md +40 -0
  185. package/skills/woocommerce-plugin-development/SKILL.md +84 -0
  186. package/skills/woocommerce-plugin-development/assets/main-plugin-file.php +28 -0
  187. package/skills/woocommerce-plugin-development/evals/examples/declare-hpos.md +9 -0
  188. package/skills/woocommerce-plugin-development/evals/examples/save-order-meta.md +9 -0
  189. package/skills/woocommerce-plugin-development/evals/tasks.json +32 -0
  190. package/skills/woocommerce-plugin-development/references/hooks.md +46 -0
  191. package/skills/woocommerce-plugin-development/references/hpos.md +84 -0
  192. package/skills/woocommerce-rest-api/SKILL.md +90 -0
  193. package/skills/woocommerce-rest-api/evals/examples/https-basic-auth.md +8 -0
  194. package/skills/woocommerce-rest-api/evals/examples/idempotent-order-create.md +7 -0
  195. package/skills/woocommerce-rest-api/evals/tasks.json +30 -0
  196. package/skills/woocommerce-rest-api/references/endpoints.md +66 -0
  197. package/skills/wordpress-plugin-security/SKILL.md +121 -0
  198. package/skills/wordpress-plugin-security/evals/examples/admin-form-nonce.md +9 -0
  199. package/skills/wordpress-plugin-security/evals/examples/private-rest-route.md +9 -0
  200. package/skills/wordpress-plugin-security/evals/tasks.json +31 -0
  201. package/skills/wordpress-plugin-security/references/review-checklist.md +31 -0
@@ -0,0 +1,30 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "facts-only",
5
+ "task": "Write a cotton shirt description that states only the catalog facts and the real price.",
6
+ "expects": {
7
+ "must_include": [
8
+ "19.00 USD",
9
+ "cotton"
10
+ ],
11
+ "must_not_include": [
12
+ "clinically proven"
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "label-generated",
18
+ "task": "Mark a generated Merchant Center description so it is not submitted as a human-written description attribute.",
19
+ "expects": {
20
+ "must_include": [
21
+ "trained_algorithmic_media",
22
+ "structured_description"
23
+ ],
24
+ "must_not_include": [
25
+ "organic cotton"
26
+ ]
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,24 @@
1
+ # Claims the description checker refuses
2
+
3
+ The checker is a lexical guard, not a review of advertising law. Merchants still need their own review for regulated goods.
4
+
5
+ Refused unless the same phrase is listed in `allowedClaims`:
6
+
7
+ - `cure` and `cures`
8
+ - `clinically proven`
9
+ - `guaranteed to`
10
+ - `100% effective`
11
+ - `risk-free`
12
+
13
+ Prices must match `/^(\d+\.\d{2}) ([A-Z]{3})$/` against the source. `19.00 USD` matches a source price of `19.00` and currency `USD`. `$19.00` is ignored by this pattern. Do not rely on that gap. The writer still must not invent a dollar amount.
14
+
15
+ ## Merchant Center labeling
16
+
17
+ | How the text was produced | Attribute |
18
+ | --- | --- |
19
+ | Written or translated by a person from the catalog | `title`, `description` |
20
+ | Generated | `structured_title` or `structured_description` with `digital_source_type` `trained_algorithmic_media` |
21
+
22
+ Omitting `digital_source_type` means Google treats the structured value as `default` (not generated). Do not omit it for generated text.
23
+
24
+ Lengths from the product data specification: title content at most 150 characters, description content at most 5000.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ const REFUSED = [
5
+ { label: "cure", pattern: /\bcures?\b/i },
6
+ { label: "clinically proven", pattern: /\bclinically proven\b/i },
7
+ { label: "guaranteed to", pattern: /\bguaranteed to\b/i },
8
+ { label: "100% effective", pattern: /\b100% effective\b/i },
9
+ { label: "risk-free", pattern: /\brisk[- ]free\b/i },
10
+ ];
11
+
12
+ export function checkProductDescription(description, source) {
13
+ const errors = [];
14
+ if (typeof description !== "string" || description.trim() === "") {
15
+ return ["Description is empty."];
16
+ }
17
+ if (description.length > 5000) {
18
+ errors.push(`Description is ${description.length} characters; the maximum is 5000.`);
19
+ }
20
+ if (!source || typeof source.price !== "string" || typeof source.currency !== "string") {
21
+ errors.push("Source price and currency are required.");
22
+ return errors;
23
+ }
24
+ const allowed = Array.isArray(source.allowedClaims) ? source.allowedClaims.join("\n") : "";
25
+ for (const rule of REFUSED) {
26
+ if (rule.pattern.test(description) && !rule.pattern.test(allowed)) {
27
+ errors.push(`Unsupported claim: ${rule.label}`);
28
+ }
29
+ }
30
+ for (const match of description.matchAll(/(\d+\.\d{2})\s+([A-Z]{3})/g)) {
31
+ if (match[1] !== source.price || match[2] !== source.currency) {
32
+ errors.push(`Price ${match[1]} ${match[2]} does not match ${source.price} ${source.currency}.`);
33
+ }
34
+ }
35
+ return errors;
36
+ }
37
+
38
+ function readOption(name) {
39
+ const index = process.argv.indexOf(name);
40
+ return index === -1 ? undefined : process.argv[index + 1];
41
+ }
42
+
43
+ const isCli = process.argv[1] && process.argv[1].endsWith("check-description.mjs");
44
+ if (isCli) {
45
+ const descriptionFile = readOption("--description");
46
+ const sourceFile = readOption("--source");
47
+ if (!descriptionFile || !sourceFile) {
48
+ console.error("Usage: node check-description.mjs --description copy.txt --source source.json");
49
+ process.exit(2);
50
+ }
51
+ const description = await readFile(descriptionFile, "utf8");
52
+ const source = JSON.parse(await readFile(sourceFile, "utf8"));
53
+ const errors = checkProductDescription(description, source);
54
+ if (errors.length > 0) {
55
+ for (const error of errors) {
56
+ console.error(`ERROR ${error}`);
57
+ }
58
+ process.exit(1);
59
+ }
60
+ console.log("Description passed the local checks.");
61
+ }
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: returns-and-refunds
3
+ description: "Run returns as a tracked RMA from request to inspection, refund only what the policy and payment allow, and restock only inspected items. Use when building a returns portal, refund rules, exchanges, store credit, or restocking after a return."
4
+ license: MIT
5
+ metadata:
6
+ author: Naser Sadrmanesh
7
+ version: "0.3.0"
8
+ ---
9
+
10
+ # Returns and refunds
11
+
12
+ A return is a small workflow with its own record, not a status on the order. Keep the return, the refund, and the stock movement separate, and link them. Statuses and the refund calculation are in [the RMA notes](references/rma-states.md).
13
+
14
+ ## Procedure
15
+
16
+ 1. Create a return record (an RMA) with the order, the lines and quantities, the reason, and the requested outcome: refund, exchange, or store credit.
17
+ 2. Check eligibility on the server: the return window from the delivery date, the product's returnable flag, quantities not already returned, and final-sale items.
18
+ 3. Approve or reject with a reason. Approval issues return instructions or a label.
19
+ 4. When the parcel arrives, inspect each line and record the condition: resellable, damaged, or missing.
20
+ 5. Refund from the inspection result, not from the request. Use the refund calculation in the notes.
21
+ 6. Restock only resellable units, as a `return` stock movement. Damaged units go to a separate location or are written off.
22
+ 7. Close the return. The order shows refunded amounts and return history but keeps its original lines.
23
+
24
+ ## Refunding
25
+
26
+ - Refund through the original payment method, using the gateway's refund API and an idempotency key per refund. Store the provider refund id.
27
+ - The total refunded on an order can never exceed what was captured. Check the sum of earlier refunds first.
28
+ - Refund tax in proportion to the refunded lines. Refund shipping only when the policy says so, such as for a wrong or damaged item.
29
+ - A partial refund leaves the order paid for the rest. Use the platform's refunded status only when everything is refunded.
30
+ - Store credit is a liability. Record it in a ledger with an expiry if the law allows one, not as a negative order.
31
+ - In WooCommerce, create refunds with `wc_create_refund()` or `POST /wp-json/wc/v3/orders/{id}/refunds`. Set `api_refund` to `true` only when the gateway should return the money; otherwise it is a manual refund record.
32
+
33
+ ## Exchanges
34
+
35
+ An exchange is a return plus a new order for the replacement, linked to the return. Reserve the replacement when the exchange is approved so it does not sell out while the parcel is in transit. Charge or refund the price difference on the new order.
36
+
37
+ ## Gotchas
38
+
39
+ - Restocking at approval puts units that never come back, or come back broken, on sale.
40
+ - Refunding at request time pays out for parcels that never arrive.
41
+ - A refund that is created only in the payment provider's dashboard leaves the shop showing the order as fully paid. Import provider refunds by webhook.
42
+ - Two staff members refunding the same line at once can exceed the captured amount. Lock the order or check the refund total inside the transaction.
43
+ - Some laws give a minimum withdrawal period for distance sales, such as 14 days in the EU. The store's window cannot be shorter for those customers.
44
+
45
+ ## Verification
46
+
47
+ - A return outside the window is refused with the reason shown.
48
+ - A line cannot be returned twice beyond its purchased quantity.
49
+ - A damaged unit is refunded per policy but not added to sellable stock.
50
+ - The sum of refunds never exceeds the captured amount, even with two concurrent requests.
51
+ - A partial refund leaves the order paid, with the refunded amount shown.
52
+
53
+ ## Evaluation
54
+
55
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,3 @@
1
+ Approve the return for two mugs and wait for the parcel. Nothing is refunded or restocked at approval.
2
+
3
+ When it arrives, inspect each unit. The intact mug is resellable: refund its paid price and tax and restock it with a `return` movement. The broken mug is damaged: if it broke in transit or was faulty, refund it per policy, but move it to a damaged location instead of sellable stock. The third mug stays with the shopper and the order stays paid for it.
@@ -0,0 +1,3 @@
1
+ Send `POST /wp-json/wc/v3/orders/{id}/refunds` with the `line_items` entry for that line, its `refund_total` and `refund_tax`, the summed `amount`, and `api_refund: true` so the gateway returns the money.
2
+
3
+ This is a partial refund. WooCommerce keeps the order processing or completed and shows the refunded amount. Check that earlier refunds plus this one do not exceed the order total before you send it.
@@ -0,0 +1,20 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "refund-after-inspection",
5
+ "task": "A shopper returns two of three mugs. One arrives broken. Explain what is refunded, what is restocked, and when.",
6
+ "expects": {
7
+ "must_include": ["inspect", "resellable", "damaged", "restock"],
8
+ "must_not_include": ["restock both at approval"]
9
+ }
10
+ },
11
+ {
12
+ "id": "woocommerce-partial-refund",
13
+ "task": "Refund one line of a WooCommerce order through the payment gateway and keep the order paid for the rest.",
14
+ "expects": {
15
+ "must_include": ["orders/{id}/refunds", "api_refund", "partial"],
16
+ "must_not_include": ["set the order status to refunded"]
17
+ }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,37 @@
1
+ # RMA states and refund calculation
2
+
3
+ ## States
4
+
5
+ | State | Next | Notes |
6
+ | --- | --- | --- |
7
+ | `requested` | `approved`, `rejected` | Created by the shopper or staff |
8
+ | `approved` | `in_transit`, `canceled` | Instructions or label issued. Reserve exchange items here. |
9
+ | `in_transit` | `received` | Carrier tracking attached |
10
+ | `received` | `inspected` | Parcel arrived |
11
+ | `inspected` | `refunded`, `exchanged`, `credited`, `rejected` | Condition recorded per line |
12
+ | `refunded`, `exchanged`, `credited`, `rejected`, `canceled` | none | Final |
13
+
14
+ Only move forward. Record who changed the state and when.
15
+
16
+ ## Refund calculation
17
+
18
+ For each inspected line:
19
+
20
+ ```text
21
+ line_refund = unit_price_paid * accepted_quantity
22
+ - restocking_fee (if the policy has one and the item was not faulty)
23
+ tax_refund = line_tax_paid * accepted_quantity / purchased_quantity
24
+ ```
25
+
26
+ - `unit_price_paid` is after discounts. An order-level coupon is spread across lines in proportion to their price, the same way the order calculated it.
27
+ - Add shipping only for faulty or wrong items, or when the whole order is returned and the policy refunds shipping.
28
+ - Round each line in minor units, then add. Cap the total at `captured - already_refunded`.
29
+
30
+ ## Records
31
+
32
+ | Record | Holds |
33
+ | --- | --- |
34
+ | Return | order id, lines, quantities, reason, state, outcome |
35
+ | Inspection | line, condition, notes, photos, inspector |
36
+ | Refund | amount, tax, shipping, provider refund id, status |
37
+ | Stock movement | `return` into a sellable or quarantine location |
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: rtl-and-persian-localization
3
+ description: "Localize a storefront for Persian (fa_IR) and right-to-left layout: logical CSS, digits, Yeh and Kaf, ZWNJ, currency display, and Gregorian storage. Use when building or reviewing Persian, Farsi, or RTL commerce UI."
4
+ license: MIT
5
+ compatibility: "The Persian normalizer requires Node.js 20 or newer. No network access."
6
+ metadata:
7
+ author: Naser Sadrmanesh
8
+ version: "0.3.0"
9
+ ---
10
+
11
+ # RTL and Persian localization
12
+
13
+ Use this skill for a Persian storefront or any right-to-left commerce UI. Search matching uses the marketplace search skill. The normalizer in [`scripts/normalize-persian.mjs`](scripts/normalize-persian.mjs) is for display and for search text. Letter and digit rules are in [the Persian text notes](references/persian-text.md).
14
+
15
+ ## Procedure
16
+
17
+ 1. Set the document language and direction. For Persian, `lang="fa"` and `dir="rtl"`. In WordPress, the `fa_IR` locale is RTL and `is_rtl()` is the check to use in PHP.
18
+ 2. Lay out with CSS logical properties. Do not mirror the whole page with a plugin that flips `left` and `right` in existing files.
19
+ 3. Store catalog text as Unicode with Persian Yeh and Kaf and with ZWNJ where the word needs it. Store dates as Gregorian timestamps. Store money as a decimal amount plus an ISO currency code.
20
+ 4. Format digits for display only. APIs, SKUs, payment amounts, and HTML attributes stay ASCII.
21
+ 5. Translate user-facing strings with the plugin text domain. Do not hard-code Persian in a PHP string that should go through gettext, and do not hard-code English in a template that Persian shoppers see.
22
+
23
+ ## Direction
24
+
25
+ - Prefer `margin-inline-start`, `padding-inline`, `inset-inline-start`, `border-inline-end`, and `text-align: start`.
26
+ - Use flex and grid. `flex-direction: row` follows the document direction. Use `row-reverse` only when the order is intentionally opposite to the language.
27
+ - Mirror directional icons such as back and forward. Do not mirror logos, payment marks, product photos, or numbers.
28
+ - Form focus order follows the visual order. Test with the keyboard, not only with a screenshot.
29
+ - WooCommerce emails that render the same template need the same direction. A storefront `dir="rtl"` does not change an email that is a separate HTML document.
30
+
31
+ ## Persian text
32
+
33
+ - Locale code: `fa_IR`.
34
+ - Fold Arabic Yeh `ي` (U+064A) to Persian Yeh `ی` (U+06CC) and Arabic Kaf `ك` (U+0643) to Persian Kaf `ک` (U+06A9) when you normalize input. Shoppers and old content use both.
35
+ - Keep ZWNJ (U+200C) in stored and displayed words (`می‌روم`). Search normalization may remove it so the two spellings match. Display normalization must not.
36
+ - Persian digits `۰۱۲۳۴۵۶۷۸۹` are a display format. Also accept Arabic-Indic digits `٠١٢٣٤٥٦٧٨٩` on input and normalize them.
37
+ - Use a font that covers Arabic script. A Latin-only stack renders Persian as missing glyphs.
38
+
39
+ Run the script:
40
+
41
+ ```bash
42
+ node scripts/normalize-persian.mjs --file title.txt
43
+ node scripts/normalize-persian.mjs --search "كالاي ۱۲"
44
+ ```
45
+
46
+ `--search` prints the match form. The default prints the display form and keeps ZWNJ. `--persian-digits` converts ASCII digits to Persian digits for display.
47
+
48
+ ## Money, dates, addresses
49
+
50
+ - WooCommerce currency for the Iranian rial is the ISO code `IRR`. Format the amount with the store's decimal settings (`wc_get_price_decimals()`), not with a hard-coded exponent.
51
+ - Toman is a display and pricing decision: 1 toman equals 10 rials. If the shop charges in toman, say that in the currency label and store a consistent unit. Do not divide amounts by 10 only in the theme while checkout charges rials.
52
+ - WordPress and WooCommerce store order dates in Gregorian, with a site timezone and a GMT field. A Jalali calendar is a presentation layer. Do not write Jalali strings into order date columns.
53
+ - Iranian addresses need country `IR`, province, city, street, and a 10-digit postal code. Do not require a state from a country list that has no Iranian provinces unless those provinces are actually configured. Add them through WooCommerce's country locale and state list rather than a one-off checkout hack.
54
+ - Phone numbers: accept a local mobile number without a North American mask. Store a normalized form you can dial, and display the local form.
55
+
56
+ ## WordPress and WooCommerce
57
+
58
+ - Put every new string in gettext with the plugin text domain. Load private translations on `init`.
59
+ - Switch field order and labels with `woocommerce_get_country_locale` and `woocommerce_localisation_address_formats` for `IR`, not by editing core templates first.
60
+ - `body` gets the `rtl` class from WordPress when the locale is RTL. Theme CSS should key off `dir` or `.rtl` and still prefer logical properties so the same stylesheet serves both directions.
61
+ - Prices in templates go through `wc_price()`. That helper uses the store currency settings. Do not concatenate a hard-coded `ریال` or `$` in front of a raw number.
62
+
63
+ ## Gotchas
64
+
65
+ - `direction: rtl` on a single inner box, while the document is LTR, traps punctuation and mixed English SKUs. Set direction on the document, and on emails, not on random cards.
66
+ - `left` and `right` in copied CSS survive an RTL locale and pin totals to the wrong side.
67
+ - Replacing every digit in the HTML document breaks CSS, JSON, and payment signatures. Convert digits in text nodes you control.
68
+ - Sorting Persian with a binary code-point order is not alphabetical. Use a Persian collation when the list is a dictionary. Do not use it for SKUs.
69
+ - Translating a string that includes the price ("Pay 19.00") needs a placeholder, not concatenation: `sprintf( __( 'Pay %s', 'example' ), $formatted )`.
70
+
71
+ ## Verification
72
+
73
+ - `fa_IR` renders the shopper-facing checkout RTL, with the total and the pay control aligned to the inline end.
74
+ - A title that contains Arabic Yeh and Persian digits round-trips through display normalization without losing ZWNJ.
75
+ - Search normalization makes `كالا` and `کالا` match. The stored title is unchanged.
76
+ - An order date stored by WooCommerce remains Gregorian. Any Jalali label is display-only.
77
+ - A test payment uses the same major units the shopper saw. Toman and rial are not mixed inside one order.
78
+
79
+ ## Evaluation
80
+
81
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,7 @@
1
+ Set the document for Persian.
2
+
3
+ ```html
4
+ <html lang="fa" dir="rtl">
5
+ ```
6
+
7
+ Use the `fa_IR` locale so `is_rtl()` is true. Space the total with `margin-inline-start`, not a physical left margin.
@@ -0,0 +1,7 @@
1
+ Format the stored amount with the store currency.
2
+
3
+ ```php
4
+ echo wp_kses_post( wc_price( $amount ) );
5
+ ```
6
+
7
+ The ISO code is `IRR` when the shop charges rials. Do not divide the charged amount in the template.
@@ -0,0 +1,30 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "document-direction",
5
+ "task": "Render the Persian storefront in the fa_IR locale with right-to-left document direction and logical CSS.",
6
+ "expects": {
7
+ "must_include": [
8
+ "fa_IR",
9
+ "dir=\"rtl\""
10
+ ],
11
+ "must_not_include": [
12
+ "margin-left"
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "rial-display",
18
+ "task": "Show an Iranian rial price through WooCommerce formatting without converting to toman only in the theme.",
19
+ "expects": {
20
+ "must_include": [
21
+ "IRR",
22
+ "wc_price"
23
+ ],
24
+ "must_not_include": [
25
+ "/ 10"
26
+ ]
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,51 @@
1
+ # Persian text handling
2
+
3
+ Use this note when you change strings, search tokens, or formatted amounts. The functions in `scripts/normalize-persian.mjs` implement the folds below.
4
+
5
+ ## Letters
6
+
7
+ | Input | Code point | Fold to |
8
+ | --- | --- | --- |
9
+ | Arabic Yeh `ي` | U+064A | Persian Yeh `ی` U+06CC |
10
+ | Persian Yeh `ی` | U+06CC | unchanged |
11
+ | Arabic Kaf `ك` | U+0643 | Persian Kaf `ک` U+06A9 |
12
+ | Persian Kaf `ک` | U+06A9 | unchanged |
13
+
14
+ Apply this fold on input and before search. Display the folded Persian letters.
15
+
16
+ ZWNJ (U+200C) is part of Persian orthography. Display and storage keep it. Search text removes it so `می‌روم` and `میروم` match. Do not replace ZWNJ with a normal space. That splits one word into two.
17
+
18
+ Arabic Teh Marbuta `ة` is not a Persian letter. Do not fold it to Heh unless you have a specific migration from Arabic copy. Leave it and show it.
19
+
20
+ ## Digits
21
+
22
+ | Glyphs | Code points | Use |
23
+ | --- | --- | --- |
24
+ | `0-9` | U+0030–U+0039 | APIs, SKUs, JSON, payment minor units, HTML attributes |
25
+ | `۰-۹` | U+06F0–U+06F9 | Optional Persian display |
26
+ | `٠-٩` | U+0660–U+0669 | Accept on input, then fold to ASCII or to Persian digits |
27
+
28
+ `toAsciiDigits` maps both Persian and Arabic-Indic digits. `toPersianDigits` maps ASCII to Persian. Do not run `toPersianDigits` on a whole HTML or JSON document.
29
+
30
+ ## Normalization entry points
31
+
32
+ - `normalizePersianDisplay(text)` folds Yeh and Kaf, keeps ZWNJ, and leaves digits unless you pass `{ digits: "persian" }` or `{ digits: "ascii" }`.
33
+ - `normalizePersianSearch(text)` folds letters, removes ZWNJ, converts digits to ASCII, lowercases Latin letters, and collapses whitespace.
34
+
35
+ Latin lowercasing is for mixed catalog titles. Persian letters have no case.
36
+
37
+ ## Currency display
38
+
39
+ ISO 4217 code in WooCommerce: `IRR` for the Iranian rial.
40
+
41
+ If the merchant prices in toman, pick one stored unit and label it. A theme-only division by 10 will not match the amount sent to the payment provider. There is no separate ISO code to assume for toman in this skill. Do not invent a code in payment requests.
42
+
43
+ Format through `wc_price()` so the decimal separator and currency symbol come from WooCommerce settings.
44
+
45
+ ## Dates
46
+
47
+ Store `date_created_gmt` as WooCommerce provides it. Format for `fa_IR` with `wp_date()` for Gregorian output in the locale. A Jalali formatter, if the shop uses one, reads that timestamp and must not write back a Jalali string.
48
+
49
+ ## Addresses
50
+
51
+ Postal codes in Iran are 10 digits. Validate the length after converting digits to ASCII. Province names should come from the configured state list for `IR` so shipping zones and the address form agree.
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ const ARABIC_YEH = /\u064A/g;
5
+ const ARABIC_KAF = /\u0643/g;
6
+ const PERSIAN_DIGITS = /[\u06F0-\u06F9]/g;
7
+ const ARABIC_INDIC_DIGITS = /[\u0660-\u0669]/g;
8
+ const ZWNJ = /\u200C/g;
9
+
10
+ export function toAsciiDigits(text) {
11
+ return text
12
+ .replace(PERSIAN_DIGITS, (digit) => String(digit.charCodeAt(0) - 0x06f0))
13
+ .replace(ARABIC_INDIC_DIGITS, (digit) => String(digit.charCodeAt(0) - 0x0660));
14
+ }
15
+
16
+ export function toPersianDigits(text) {
17
+ return text.replace(/[0-9]/g, (digit) => String.fromCharCode(0x06f0 + Number(digit)));
18
+ }
19
+
20
+ function foldLetters(text) {
21
+ return text.normalize("NFC").replace(ARABIC_YEH, "\u06CC").replace(ARABIC_KAF, "\u06A9");
22
+ }
23
+
24
+ /** Display text: fold Arabic Yeh/Kaf to Persian, keep ZWNJ, optionally use Persian digits. */
25
+ export function normalizePersianDisplay(text, { digits = "keep" } = {}) {
26
+ const folded = foldLetters(String(text));
27
+ if (digits === "persian") {
28
+ return toPersianDigits(toAsciiDigits(folded));
29
+ }
30
+ if (digits === "ascii") {
31
+ return toAsciiDigits(folded);
32
+ }
33
+ return folded;
34
+ }
35
+
36
+ /**
37
+ * Search text: fold letters, drop ZWNJ so می‌روم matches میروم, ASCII digits,
38
+ * and collapse whitespace. Latin letters are lowercased. Persian has no case.
39
+ */
40
+ export function normalizePersianSearch(text) {
41
+ const folded = toAsciiDigits(foldLetters(String(text))).replace(ZWNJ, "").toLowerCase();
42
+ return folded.replace(/\s+/g, " ").trim();
43
+ }
44
+
45
+ const isCli = process.argv[1] && process.argv[1].endsWith("normalize-persian.mjs");
46
+ if (isCli) {
47
+ const mode = process.argv.includes("--search") ? "search" : "display";
48
+ const fileFlag = process.argv.indexOf("--file");
49
+ const input =
50
+ fileFlag !== -1 && process.argv[fileFlag + 1]
51
+ ? await readFile(process.argv[fileFlag + 1], "utf8")
52
+ : process.argv.slice(2).filter((arg) => !arg.startsWith("--")).join(" ");
53
+ if (!input) {
54
+ console.error("Usage: node normalize-persian.mjs [--search] [--file input.txt] [text]");
55
+ process.exit(2);
56
+ }
57
+ const output =
58
+ mode === "search"
59
+ ? normalizePersianSearch(input)
60
+ : normalizePersianDisplay(input, { digits: process.argv.includes("--persian-digits") ? "persian" : "keep" });
61
+ process.stdout.write(output);
62
+ }
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: shopify-app-development
3
+ description: "Build a Shopify app with the configured Admin API version, session-token auth for embedded admin requests, and HMAC-verified webhooks. Use when creating or changing a Shopify app, Admin GraphQL call, app proxy, or Shopify webhook handler."
4
+ license: MIT
5
+ metadata:
6
+ author: Naser Sadrmanesh
7
+ version: "0.3.0"
8
+ ---
9
+
10
+ # Shopify app development
11
+
12
+ Use this skill for a Shopify app. Payment capture inside a shop you own is the payment skill. Webhook durability is the order and webhook skill. Storefront browsing without an admin token is the headless storefront skill.
13
+
14
+ Shopify's developer docs are the source for request authentication and webhook delivery. Re-check them when an API version or header name is load-bearing. This skill records the contracts those docs describe.
15
+
16
+ ## Procedure
17
+
18
+ 1. Keep app configuration in `shopify.app.toml`, including the API version and app-specific webhook subscriptions. Do not hard-code an API version in a handler. The `X-Shopify-API-Version` header says which version serialized that delivery, and the app configuration selects it.
19
+ 2. Authenticate every request before using a shop domain from the query, body, or header.
20
+ 3. Call the GraphQL Admin API for new admin work. Do not start a new REST Admin API client.
21
+ 4. Ask for the narrowest access scopes the mutation needs.
22
+ 5. Verify HTTPS webhooks with the app client secret over the raw body, then dedupe on `X-Shopify-Webhook-Id`.
23
+
24
+ Header and auth details are in [the webhook and auth notes](references/auth-and-webhooks.md). Worked examples and checks are in [evals/tasks.json](evals/tasks.json).
25
+
26
+ ## Which credential
27
+
28
+ | Request | Proof |
29
+ | --- | --- |
30
+ | Embedded app in the Shopify admin | Session token (ID token) validated for signature, expiry, audience, and destination. |
31
+ | HTTPS webhook | HMAC-SHA256 of the raw body, Base64, in `X-Shopify-Hmac-Sha256`, keyed with the app client secret. Compare header names case-insensitively. |
32
+ | App proxy from a storefront | App proxy signature. It proves the shop, not the person. Do not change shop settings from an app proxy route. |
33
+ | Checkout or customer-account UI extension | That extension's session token. |
34
+ | Browser call that skips the app proxy | Anonymous. It may read public data. It must not mutate the shop. |
35
+
36
+ Reject a request that fails its check. Do not fall back to a `shop` parameter.
37
+
38
+ ## Webhooks
39
+
40
+ Compute Base64 HMAC-SHA256 over the raw body with the client secret. If a JSON parser runs first, the signature will not match. Return 401 when the signature does not match.
41
+
42
+ `X-Shopify-Webhook-Id` identifies one delivery. Store it and skip a repeat. `X-Shopify-Event-Id` is shared by deliveries from the same merchant action and is not a substitute for the delivery id when you are deduping retries of one POST.
43
+
44
+ Topic and shop-domain headers are untrusted until the HMAC matches. App-specific subscriptions in `shopify.app.toml` are kept in sync by Shopify. Use `webhookSubscriptionCreate` when the subscription has to be shop-specific. You cannot set the webhook API version on that mutation; it follows the app's webhook API version.
45
+
46
+ After the app client secret rotates, Shopify can keep signing with the previous secret for up to an hour. Accept that window only if you still have the previous secret stored for verification.
47
+
48
+ Google Cloud Pub/Sub and Amazon EventBridge deliveries do not use this HMAC header. Do not pretend an HTTPS verifier applies to those transports.
49
+
50
+ ## Admin writes
51
+
52
+ - Send the shop domain you got from the verified token, not from a form field.
53
+ - Treat Admin GraphQL user errors and top-level errors as separate. A 200 response can still contain `userErrors`.
54
+ - Write webhooks and billing changes so a second delivery does not create a second subscription or a second charge.
55
+ - Do not log the access token, the client secret, or the raw HMAC.
56
+
57
+ ## Gotchas
58
+
59
+ - `express.json()` before verification replaces the raw body. Capture the bytes first.
60
+ - A shop domain check must be anchored. `example.myshopify.com.attacker.example` is not a shop domain.
61
+ - OAuth `state` has to be bound to the browser that started the install and checked on return.
62
+ - Scopes are not implied by installation. A missing scope is a failed call, not a reason to use a broader token you stored earlier.
63
+
64
+ ## Verification
65
+
66
+ - A webhook with a changed byte returns 401 and writes nothing.
67
+ - The same `X-Shopify-Webhook-Id` applied twice creates one record.
68
+ - An embedded request with no valid session token does not run a mutation.
69
+ - An app proxy request cannot change shop settings.
70
+
71
+ ## Evaluation
72
+
73
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,3 @@
1
+ Store `X-Shopify-Webhook-Id` after the HMAC matches.
2
+
3
+ If that id is already in the inbox, return 200 and do not create another order record. Authorize the handler with the verified HMAC, not with an unverified shop field.
@@ -0,0 +1,3 @@
1
+ Compute the HMAC over the raw body with the app client secret and compare it to `X-Shopify-Hmac-Sha256`.
2
+
3
+ Reject the request when the digest does not match. Read `X-Shopify-Topic` only after that check.
@@ -0,0 +1,29 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "verify-webhook",
5
+ "task": "Verify a Shopify HTTPS webhook with the app client secret before reading the topic or shop domain.",
6
+ "expects": {
7
+ "must_include": [
8
+ "X-Shopify-Hmac-Sha256",
9
+ "raw body"
10
+ ],
11
+ "must_not_include": [
12
+ "express.json()"
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "dedupe-delivery",
18
+ "task": "Process a retried Shopify order webhook once using the delivery id Shopify sends.",
19
+ "expects": {
20
+ "must_include": [
21
+ "X-Shopify-Webhook-Id"
22
+ ],
23
+ "must_not_include": [
24
+ "shop query parameter"
25
+ ]
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,44 @@
1
+ # Shopify auth and webhooks
2
+
3
+ These notes follow Shopify's app authentication and webhook delivery docs. Header names are case-insensitive on the wire.
4
+
5
+ ## HTTPS webhook headers
6
+
7
+ | Header | Use |
8
+ | --- | --- |
9
+ | `X-Shopify-Topic` | Topic, such as `products/update`. Untrusted until the HMAC matches. |
10
+ | `X-Shopify-Hmac-Sha256` | Base64 HMAC-SHA256 of the raw body using the app client secret. |
11
+ | `X-Shopify-Shop-Domain` | `myshopify.com` domain. Untrusted until the HMAC matches. |
12
+ | `X-Shopify-API-Version` | Version used to serialize the payload. |
13
+ | `X-Shopify-Webhook-Id` | Unique per delivery. Dedupe retries with this. |
14
+ | `X-Shopify-Triggered-At` | When Shopify triggered the delivery. |
15
+ | `X-Shopify-Event-Id` | Shared by deliveries from the same merchant action. |
16
+
17
+ Manual check:
18
+
19
+ ```js
20
+ import { createHmac, timingSafeEqual } from "node:crypto";
21
+
22
+ export function shopifyWebhookValid(secret, rawBody, header) {
23
+ const digest = createHmac("sha256", secret).update(rawBody).digest();
24
+ const received = Buffer.from(String(header), "base64");
25
+ if (digest.length !== received.length) {
26
+ return false;
27
+ }
28
+ return timingSafeEqual(digest, received);
29
+ }
30
+ ```
31
+
32
+ Compare the decoded digest bytes. A string compare of the Base64 text can disagree on padding. Reject when `header` is missing.
33
+
34
+ ## Session tokens
35
+
36
+ For an embedded admin request, validate the ID token's signature, expiry, audience, and destination. The destination shop is the shop you may call. A `shop` query parameter is not that proof.
37
+
38
+ ## App configuration
39
+
40
+ Prefer app-specific webhook subscriptions in `shopify.app.toml` when every install should receive the same topics. Use the Admin GraphQL `webhookSubscriptionCreate` mutation for a subscription that differs per shop. The webhook API version is the app's configured version in both cases.
41
+
42
+ ## Shop domain
43
+
44
+ Accept a shop domain only when it matches a pattern anchored at both ends, such as a single label plus `.myshopify.com`. Do not use a substring or a suffix check that allows an extra domain on the right.