@hostwebhook/node-types 1.74.0 → 1.75.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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DRIVE_OPERATION_SPECS = exports.DRIVE_OPERATIONS = exports.GOOGLE_CALENDAR_TOOLKIT_BY_TOOL_NAME = exports.GOOGLE_CALENDAR_TOOLKIT_SPECS = exports.isGoogleCalendarOperation = exports.GOOGLE_CALENDAR_OPERATION_SPECS = exports.GOOGLE_CALENDAR_OPERATIONS = exports.isGmailOperation = exports.resolveGmailSendFields = exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATIONS = exports.versionCatalogErrors = exports.fieldsLost = exports.fieldsLostBetween = exports.currentVersion = exports.versionSpec = exports.versionsOf = exports.isVersioned = exports.NODE_TYPE_TO_PREFIX = exports.PREFIX_TO_NODE_TYPE = exports.NODE_STATE_KEYS = exports.NODE_COLORS = exports.NODE_DETAIL_PATHS = exports.getNodeRegistryEntry = exports.NODE_REGISTRY = exports.getNodeDispatchConfig = exports.getAllNodeCollections = exports.NODE_DISPATCH = exports.resolveNodeId = exports.PREFIX_TO_TYPE = exports.NODE_UI = exports.ALL_NODE_TYPES = exports.isNodeType = exports.isTerminal = exports.canSendToNodes = exports.canReceiveFromNodes = exports.canReceiveFrom = exports.NODE_CONNECTIONS = exports.iterableMeta = exports.singleMeta = void 0;
4
- exports.operacionesDeSlackPara = exports.SLACK_CAPACIDADES_POR_CREDENCIAL = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isBucketOperation = exports.BUCKET_ITERABLE_OPERATIONS = exports.BUCKET_OPERATION_SPECS = exports.BUCKET_OPERATIONS = exports.isJiraOperation = exports.JIRA_ITERABLE_OPERATIONS = exports.JIRA_DROPDOWN_OPERATIONS = exports.JIRA_OPERATION_SPECS = exports.JIRA_OPERATIONS = exports.isGithubOperation = exports.GITHUB_ITERABLE_OPERATIONS = exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATION_SPECS = exports.GITHUB_OPERATIONS = exports.isShopifyOperation = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_OPERATIONS = exports.isMailchimpOperation = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_OPERATIONS = exports.herramientasDeDiscordPara = exports.DISCORD_TOOLKIT_BY_TOOL_NAME = exports.DISCORD_TOOLKIT_SPECS = exports.isDiscordOperation = exports.camposDeDiscordNoDisponibles = exports.discordPuedeEjecutar = exports.operacionesDeDiscordPara = exports.DISCORD_CAPACIDADES_POR_CREDENCIAL = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.camposNoDisponiblesPara = exports.puedeEjecutar = exports.operacionesPara = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = void 0;
5
- exports.CREDENTIAL_TYPES = exports.ventanaDeContextoDeOpenRouter = exports.opcionesDeModelosDeOpenRouter = exports.URL_DE_MODELOS_DE_OPENROUTER = exports.getModelLabel = exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isNotionOperation = exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.herramientasDeSlackPara = exports.SLACK_TOOLKIT_BY_TOOL_NAME = exports.SLACK_TOOLKIT_SPECS = exports.isSlackOperation = exports.camposDeSlackNoDisponibles = exports.slackPuedeEjecutar = void 0;
6
- exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = void 0;
4
+ exports.isBucketOperation = exports.BUCKET_ITERABLE_OPERATIONS = exports.BUCKET_OPERATION_SPECS = exports.BUCKET_OPERATIONS = exports.isJiraOperation = exports.JIRA_ITERABLE_OPERATIONS = exports.JIRA_DROPDOWN_OPERATIONS = exports.JIRA_OPERATION_SPECS = exports.JIRA_OPERATIONS = exports.isGithubOperation = exports.GITHUB_ITERABLE_OPERATIONS = exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATION_SPECS = exports.GITHUB_OPERATIONS = exports.scopesQueFaltanEnShopify = exports.isShopifyOperation = exports.SHOPIFY_PRODUCT_STATUSES = exports.SHOPIFY_CANCEL_REASONS = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATION_SCOPES = exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_OPERATIONS = exports.isMailchimpOperation = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_OPERATIONS = exports.herramientasDeDiscordPara = exports.DISCORD_TOOLKIT_BY_TOOL_NAME = exports.DISCORD_TOOLKIT_SPECS = exports.isDiscordOperation = exports.camposDeDiscordNoDisponibles = exports.discordPuedeEjecutar = exports.operacionesDeDiscordPara = exports.DISCORD_CAPACIDADES_POR_CREDENCIAL = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.camposNoDisponiblesPara = exports.puedeEjecutar = exports.operacionesPara = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = void 0;
5
+ exports.getModelLabel = exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isNotionOperation = exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.herramientasDeSlackPara = exports.SLACK_TOOLKIT_BY_TOOL_NAME = exports.SLACK_TOOLKIT_SPECS = exports.isSlackOperation = exports.camposDeSlackNoDisponibles = exports.slackPuedeEjecutar = exports.operacionesDeSlackPara = exports.SLACK_CAPACIDADES_POR_CREDENCIAL = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = void 0;
6
+ exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.ventanaDeContextoDeOpenRouter = exports.opcionesDeModelosDeOpenRouter = exports.URL_DE_MODELOS_DE_OPENROUTER = void 0;
7
7
  var types_js_1 = require("./types.js");
8
8
  Object.defineProperty(exports, "singleMeta", { enumerable: true, get: function () { return types_js_1.singleMeta; } });
9
9
  Object.defineProperty(exports, "iterableMeta", { enumerable: true, get: function () { return types_js_1.iterableMeta; } });
@@ -115,9 +115,13 @@ Object.defineProperty(exports, "isMailchimpOperation", { enumerable: true, get:
115
115
  var shopify_operations_js_1 = require("./shopify-operations.js");
116
116
  Object.defineProperty(exports, "SHOPIFY_OPERATIONS", { enumerable: true, get: function () { return shopify_operations_js_1.SHOPIFY_OPERATIONS; } });
117
117
  Object.defineProperty(exports, "SHOPIFY_OPERATION_SPECS", { enumerable: true, get: function () { return shopify_operations_js_1.SHOPIFY_OPERATION_SPECS; } });
118
+ Object.defineProperty(exports, "SHOPIFY_OPERATION_SCOPES", { enumerable: true, get: function () { return shopify_operations_js_1.SHOPIFY_OPERATION_SCOPES; } });
118
119
  Object.defineProperty(exports, "SHOPIFY_TAGGABLE_RESOURCES", { enumerable: true, get: function () { return shopify_operations_js_1.SHOPIFY_TAGGABLE_RESOURCES; } });
119
120
  Object.defineProperty(exports, "SHOPIFY_SEARCHABLE_RESOURCES", { enumerable: true, get: function () { return shopify_operations_js_1.SHOPIFY_SEARCHABLE_RESOURCES; } });
121
+ Object.defineProperty(exports, "SHOPIFY_CANCEL_REASONS", { enumerable: true, get: function () { return shopify_operations_js_1.SHOPIFY_CANCEL_REASONS; } });
122
+ Object.defineProperty(exports, "SHOPIFY_PRODUCT_STATUSES", { enumerable: true, get: function () { return shopify_operations_js_1.SHOPIFY_PRODUCT_STATUSES; } });
120
123
  Object.defineProperty(exports, "isShopifyOperation", { enumerable: true, get: function () { return shopify_operations_js_1.isShopifyOperation; } });
124
+ Object.defineProperty(exports, "scopesQueFaltanEnShopify", { enumerable: true, get: function () { return shopify_operations_js_1.scopesQueFaltanEnShopify; } });
121
125
  var github_operations_js_1 = require("./github-operations.js");
122
126
  Object.defineProperty(exports, "GITHUB_OPERATIONS", { enumerable: true, get: function () { return github_operations_js_1.GITHUB_OPERATIONS; } });
123
127
  Object.defineProperty(exports, "GITHUB_OPERATION_SPECS", { enumerable: true, get: function () { return github_operations_js_1.GITHUB_OPERATION_SPECS; } });
@@ -2,18 +2,24 @@
2
2
  * Shopify Admin API operations — single source of truth across the api, the
3
3
  * dashboard and downstream consumers (MCP server).
4
4
  *
5
- * ## Four, out of several hundred
5
+ * ## Ten, out of several hundred
6
6
  *
7
- * The Admin API has hundreds of mutations. This node exposes four, and the
7
+ * The Admin API has hundreds of mutations. This node exposes ten, and the
8
8
  * choice is argued in `api/docs/ADR-0007-shopify.md`: a menu of everything is
9
- * how a node becomes unusable, and these four cover what people actually
10
- * automate — put a customer in the store, tag something, sync stock, and read
11
- * back what the store already knows.
9
+ * how a node becomes unusable.
12
10
  *
13
- * Product creation and order fulfilment were considered and left out of v1.
14
- * A product means variants, media and prices; a fulfilment means a location, a
15
- * fulfilment service and a state machine. Each is a configuration surface of
16
- * its own, not a field.
11
+ * The first four cover what people automate on day one — put a customer in the
12
+ * store, tag something, sync stock, and read back what the store already
13
+ * knows. The six added on 2026-09-08 cover what they ask for on day two: the
14
+ * order lifecycle (draft → complete → fulfil → cancel), custom data
15
+ * (metafields) and putting a product in the catalogue.
16
+ *
17
+ * ⚠️ **Every entry below carries the OAuth scope it needs**, in
18
+ * `SHOPIFY_OPERATION_SCOPES`. That is not documentation: a scope missing from
19
+ * the credential answers HTTP 403 on a call that looks perfectly formed, and
20
+ * on Shopify a scope is granted at INSTALL time — adding one to the app means
21
+ * every existing credential has to be reconnected before it can use the new
22
+ * operation.
17
23
  *
18
24
  * ## GraphQL only, and the version is not ours to drift on
19
25
  *
@@ -54,8 +60,35 @@
54
60
  * `inventoryAdjustQuantities` must be sent with the `@idempotent` directive
55
61
  * and an idempotency key. A stock adjustment is the one operation here where a
56
62
  * silent retry is a real inventory error, so this is not boilerplate.
63
+ *
64
+ * ## What was verified for the six added on 2026-09-08
65
+ *
66
+ * Read off the Admin GraphQL reference for the current version, not recalled.
67
+ * The pages, and the one fact from each that a from-memory version gets wrong:
68
+ *
69
+ * - `draftOrderCreate(input: DraftOrderInput!)` — `lineItems` is the only
70
+ * required field of the input, and each item is `{ variantId, quantity }`
71
+ * OR `{ title, originalUnitPrice, quantity }` for a custom line.
72
+ * - `draftOrderComplete(id: ID!, …)` — the id is a **sibling argument**, not
73
+ * inside an input. `paymentPending` is deprecated and is not offered here.
74
+ * - `fulfillmentCreate(fulfillment: FulfillmentInput!)` — takes
75
+ * `lineItemsByFulfillmentOrder`, NOT an order id. There is no mutation that
76
+ * fulfils "an order": fulfilment hangs off FulfillmentOrder, so the
77
+ * executor has to look them up first. That indirection is the whole reason
78
+ * ADR-0007 left this out of v1.
79
+ * - `orderCancel(orderId:, reason:, restock:, …)` — `reason` and `restock` are
80
+ * **required arguments**, and the payload's errors arrive in
81
+ * `orderCancelUserErrors`; plain `userErrors` is deprecated there.
82
+ * - `metafieldsSet(metafields: [MetafieldsSetInput!]!)` — takes a LIST, and
83
+ * each entry needs all of `ownerId`, `namespace`, `key`, `type`, `value`.
84
+ * `type` is not guessable from the value: `single_line_text_field` and
85
+ * `number_integer` are different metafields.
86
+ * - `productCreate(product: ProductCreateInput!)` — the argument is `product`,
87
+ * not `input`; `input: ProductInput` is the deprecated spelling. It creates
88
+ * ONE default variant; more variants are `productVariantsBulkCreate`, which
89
+ * is a surface of its own and is not offered here.
57
90
  */
58
- export declare const SHOPIFY_OPERATIONS: readonly ["upsertCustomer", "setTags", "adjustInventory", "findRecords"];
91
+ export declare const SHOPIFY_OPERATIONS: readonly ["upsertCustomer", "setTags", "adjustInventory", "findRecords", "createDraftOrder", "completeDraftOrder", "fulfillOrder", "cancelOrder", "setMetafield", "createProduct"];
59
92
  export type ShopifyOperation = (typeof SHOPIFY_OPERATIONS)[number];
60
93
  /** Type guard — for DTOs and AI tool calls, where the input is untrusted. */
61
94
  export declare function isShopifyOperation(value: unknown): value is ShopifyOperation;
@@ -72,6 +105,22 @@ export type ShopifyTaggableResource = (typeof SHOPIFY_TAGGABLE_RESOURCES)[number
72
105
  /** What `findRecords` can read back. */
73
106
  export declare const SHOPIFY_SEARCHABLE_RESOURCES: readonly ["orders", "customers", "products"];
74
107
  export type ShopifySearchableResource = (typeof SHOPIFY_SEARCHABLE_RESOURCES)[number];
108
+ /**
109
+ * `OrderCancelReason`, the whole enum, in Shopify's own spelling.
110
+ *
111
+ * A dropdown and not free text — and that is the opposite of the call made for
112
+ * `adjustInventory`'s `reason`, on purpose. The difference is not taste: the
113
+ * inventory vocabulary was NOT verifiable from the reference (see the note
114
+ * above), so a dropdown there would have been a list of invented values. This
115
+ * one IS the enumeration, read off `enums/OrderCancelReason`, and an argument
116
+ * of type `OrderCancelReason!` rejects anything outside it — so free text here
117
+ * would only mean the user finds out by failing.
118
+ */
119
+ export declare const SHOPIFY_CANCEL_REASONS: readonly ["CUSTOMER", "DECLINED", "FRAUD", "INVENTORY", "OTHER", "STAFF"];
120
+ export type ShopifyCancelReason = (typeof SHOPIFY_CANCEL_REASONS)[number];
121
+ /** `ProductStatus`, the whole enum. Same reasoning as the cancel reasons. */
122
+ export declare const SHOPIFY_PRODUCT_STATUSES: readonly ["ACTIVE", "DRAFT", "ARCHIVED"];
123
+ export type ShopifyProductStatus = (typeof SHOPIFY_PRODUCT_STATUSES)[number];
75
124
  export interface ShopifyParamSpec {
76
125
  /** Field key — also the property name on operationConfig. */
77
126
  name: string;
@@ -80,7 +129,7 @@ export interface ShopifyParamSpec {
80
129
  * picker backed by a live lookup of the store's locations; `gid` is a
81
130
  * Shopify global id, which is almost always templated from the payload
82
131
  * rather than typed. */
83
- type: 'gid' | 'location' | 'email' | 'string' | 'number' | 'tags' | 'taggableResource' | 'searchableResource' | 'json' | 'boolean';
132
+ type: 'gid' | 'location' | 'email' | 'string' | 'number' | 'tags' | 'taggableResource' | 'searchableResource' | 'cancelReason' | 'productStatus' | 'json' | 'boolean';
84
133
  required?: boolean;
85
134
  description: string;
86
135
  placeholder?: string;
@@ -93,3 +142,55 @@ export interface ShopifyOperationSpec {
93
142
  params: ShopifyParamSpec[];
94
143
  }
95
144
  export declare const SHOPIFY_OPERATION_SPECS: Record<ShopifyOperation, ShopifyOperationSpec>;
145
+ /**
146
+ * The OAuth scope each operation needs, so a missing one is named BEFORE the
147
+ * call instead of arriving as an HTTP 403 on a request that looks fine.
148
+ *
149
+ * ## The list is "any one of", not "all of"
150
+ *
151
+ * That is `fulfillOrder`'s doing and it is not a generalisation for its own
152
+ * sake: which fulfilment-order scope applies depends on where the order is
153
+ * fulfilled from — the merchant's own locations, a third-party service, or the
154
+ * app itself acting as one. Shopify accepts the mutation if the token carries
155
+ * ANY of the three. Requiring all three would refuse a store that is correctly
156
+ * set up.
157
+ *
158
+ * ## An empty list means "not checkable from the operation alone"
159
+ *
160
+ * `setTags` tags an order, a customer or a product through the same mutation,
161
+ * and `setMetafield` writes onto whichever resource the owner id points at —
162
+ * so the scope depends on a FIELD, not on the operation. Guessing would be
163
+ * worse than not checking: a wrong guess blocks a call Shopify would have
164
+ * accepted, and a pre-flight check that produces false refusals gets deleted.
165
+ * Those fall through to Shopify's own 403, which `describeShopifyError`
166
+ * already explains.
167
+ *
168
+ * ⚠️ Read against `shopify.dev/docs/api/usage/access-scopes` and each
169
+ * mutation's own "Access requirements". Two things follow from that page and
170
+ * are relied on by `scopesQueFaltanEnShopify`:
171
+ *
172
+ * - **A write scope includes read.** `write_orders` grants `read_orders`, so
173
+ * a required `read_x` is satisfied by a granted `write_x`.
174
+ * - **Scopes are granted at install.** Adding one to the app does not give
175
+ * it to credentials that already exist; those have to be reconnected.
176
+ */
177
+ export declare const SHOPIFY_OPERATION_SCOPES: Record<ShopifyOperation, readonly string[]>;
178
+ /**
179
+ * Which of an operation's scopes the credential does NOT have.
180
+ *
181
+ * Empty means "nothing to say" — and it says that in three different
182
+ * situations, all of which have to fail OPEN:
183
+ *
184
+ * 1. The operation declares no scope (the resource-dependent ones).
185
+ * 2. The credential has a scope string and it covers one of the alternatives.
186
+ * 3. **The credential has no scope string at all.** Shopify returns the
187
+ * granted scopes on the token exchange, but a credential stored before
188
+ * that was read, or one whose metadata was pruned, has an empty string —
189
+ * and refusing to run a node because we cannot see its permissions would
190
+ * break working flows to prevent a maybe. Shopify is the authority here;
191
+ * this check only saves the round trip when it can prove the answer.
192
+ *
193
+ * @param concedidos what Shopify granted, as it sends it: a comma-separated
194
+ * string, or the already-split list.
195
+ */
196
+ export declare function scopesQueFaltanEnShopify(concedidos: string | readonly string[] | null | undefined, requeridos: readonly string[]): string[];
@@ -3,18 +3,24 @@
3
3
  * Shopify Admin API operations — single source of truth across the api, the
4
4
  * dashboard and downstream consumers (MCP server).
5
5
  *
6
- * ## Four, out of several hundred
6
+ * ## Ten, out of several hundred
7
7
  *
8
- * The Admin API has hundreds of mutations. This node exposes four, and the
8
+ * The Admin API has hundreds of mutations. This node exposes ten, and the
9
9
  * choice is argued in `api/docs/ADR-0007-shopify.md`: a menu of everything is
10
- * how a node becomes unusable, and these four cover what people actually
11
- * automate — put a customer in the store, tag something, sync stock, and read
12
- * back what the store already knows.
10
+ * how a node becomes unusable.
13
11
  *
14
- * Product creation and order fulfilment were considered and left out of v1.
15
- * A product means variants, media and prices; a fulfilment means a location, a
16
- * fulfilment service and a state machine. Each is a configuration surface of
17
- * its own, not a field.
12
+ * The first four cover what people automate on day one — put a customer in the
13
+ * store, tag something, sync stock, and read back what the store already
14
+ * knows. The six added on 2026-09-08 cover what they ask for on day two: the
15
+ * order lifecycle (draft → complete → fulfil → cancel), custom data
16
+ * (metafields) and putting a product in the catalogue.
17
+ *
18
+ * ⚠️ **Every entry below carries the OAuth scope it needs**, in
19
+ * `SHOPIFY_OPERATION_SCOPES`. That is not documentation: a scope missing from
20
+ * the credential answers HTTP 403 on a call that looks perfectly formed, and
21
+ * on Shopify a scope is granted at INSTALL time — adding one to the app means
22
+ * every existing credential has to be reconnected before it can use the new
23
+ * operation.
18
24
  *
19
25
  * ## GraphQL only, and the version is not ours to drift on
20
26
  *
@@ -55,15 +61,49 @@
55
61
  * `inventoryAdjustQuantities` must be sent with the `@idempotent` directive
56
62
  * and an idempotency key. A stock adjustment is the one operation here where a
57
63
  * silent retry is a real inventory error, so this is not boilerplate.
64
+ *
65
+ * ## What was verified for the six added on 2026-09-08
66
+ *
67
+ * Read off the Admin GraphQL reference for the current version, not recalled.
68
+ * The pages, and the one fact from each that a from-memory version gets wrong:
69
+ *
70
+ * - `draftOrderCreate(input: DraftOrderInput!)` — `lineItems` is the only
71
+ * required field of the input, and each item is `{ variantId, quantity }`
72
+ * OR `{ title, originalUnitPrice, quantity }` for a custom line.
73
+ * - `draftOrderComplete(id: ID!, …)` — the id is a **sibling argument**, not
74
+ * inside an input. `paymentPending` is deprecated and is not offered here.
75
+ * - `fulfillmentCreate(fulfillment: FulfillmentInput!)` — takes
76
+ * `lineItemsByFulfillmentOrder`, NOT an order id. There is no mutation that
77
+ * fulfils "an order": fulfilment hangs off FulfillmentOrder, so the
78
+ * executor has to look them up first. That indirection is the whole reason
79
+ * ADR-0007 left this out of v1.
80
+ * - `orderCancel(orderId:, reason:, restock:, …)` — `reason` and `restock` are
81
+ * **required arguments**, and the payload's errors arrive in
82
+ * `orderCancelUserErrors`; plain `userErrors` is deprecated there.
83
+ * - `metafieldsSet(metafields: [MetafieldsSetInput!]!)` — takes a LIST, and
84
+ * each entry needs all of `ownerId`, `namespace`, `key`, `type`, `value`.
85
+ * `type` is not guessable from the value: `single_line_text_field` and
86
+ * `number_integer` are different metafields.
87
+ * - `productCreate(product: ProductCreateInput!)` — the argument is `product`,
88
+ * not `input`; `input: ProductInput` is the deprecated spelling. It creates
89
+ * ONE default variant; more variants are `productVariantsBulkCreate`, which
90
+ * is a surface of its own and is not offered here.
58
91
  */
59
92
  Object.defineProperty(exports, "__esModule", { value: true });
60
- exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATIONS = void 0;
93
+ exports.SHOPIFY_OPERATION_SCOPES = exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_PRODUCT_STATUSES = exports.SHOPIFY_CANCEL_REASONS = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATIONS = void 0;
61
94
  exports.isShopifyOperation = isShopifyOperation;
95
+ exports.scopesQueFaltanEnShopify = scopesQueFaltanEnShopify;
62
96
  exports.SHOPIFY_OPERATIONS = [
63
97
  'upsertCustomer',
64
98
  'setTags',
65
99
  'adjustInventory',
66
100
  'findRecords',
101
+ 'createDraftOrder',
102
+ 'completeDraftOrder',
103
+ 'fulfillOrder',
104
+ 'cancelOrder',
105
+ 'setMetafield',
106
+ 'createProduct',
67
107
  ];
68
108
  /** Type guard — for DTOs and AI tool calls, where the input is untrusted. */
69
109
  function isShopifyOperation(value) {
@@ -90,6 +130,27 @@ exports.SHOPIFY_SEARCHABLE_RESOURCES = [
90
130
  'customers',
91
131
  'products',
92
132
  ];
133
+ /**
134
+ * `OrderCancelReason`, the whole enum, in Shopify's own spelling.
135
+ *
136
+ * A dropdown and not free text — and that is the opposite of the call made for
137
+ * `adjustInventory`'s `reason`, on purpose. The difference is not taste: the
138
+ * inventory vocabulary was NOT verifiable from the reference (see the note
139
+ * above), so a dropdown there would have been a list of invented values. This
140
+ * one IS the enumeration, read off `enums/OrderCancelReason`, and an argument
141
+ * of type `OrderCancelReason!` rejects anything outside it — so free text here
142
+ * would only mean the user finds out by failing.
143
+ */
144
+ exports.SHOPIFY_CANCEL_REASONS = [
145
+ 'CUSTOMER',
146
+ 'DECLINED',
147
+ 'FRAUD',
148
+ 'INVENTORY',
149
+ 'OTHER',
150
+ 'STAFF',
151
+ ];
152
+ /** `ProductStatus`, the whole enum. Same reasoning as the cancel reasons. */
153
+ exports.SHOPIFY_PRODUCT_STATUSES = ['ACTIVE', 'DRAFT', 'ARCHIVED'];
93
154
  exports.SHOPIFY_OPERATION_SPECS = {
94
155
  upsertCustomer: {
95
156
  label: 'Add or update customer',
@@ -260,4 +321,348 @@ exports.SHOPIFY_OPERATION_SPECS = {
260
321
  },
261
322
  ],
262
323
  },
324
+ createDraftOrder: {
325
+ label: 'Create draft order',
326
+ description: 'Build an order the merchant can review, invoice or complete. This is the supported way to put an order into a store from outside — it does not charge anybody by itself.',
327
+ apiRoute: 'mutation draftOrderCreate',
328
+ params: [
329
+ {
330
+ name: 'lineItems',
331
+ label: 'Line items',
332
+ type: 'json',
333
+ required: true,
334
+ description: 'A JSON array. Each entry is either {"variantId":"gid://shopify/ProductVariant/…","quantity":1} for something in the catalogue, or {"title":"…","originalUnitPrice":"10.00","quantity":1} for a custom line. An empty array is refused by Shopify, not by us.',
335
+ placeholder: '[{"variantId":"gid://shopify/ProductVariant/1234","quantity":1}]',
336
+ },
337
+ {
338
+ name: 'email',
339
+ label: 'Customer email',
340
+ type: 'email',
341
+ description: 'Who the draft is for, when you do not already have their Shopify id. Ignored if you set a customer id.',
342
+ placeholder: '{{payload.email}}',
343
+ },
344
+ {
345
+ name: 'customerId',
346
+ label: 'Customer id',
347
+ type: 'gid',
348
+ description: 'gid://shopify/Customer/1234. Wins over the email — with both set, Shopify attaches the draft to this customer.',
349
+ placeholder: '{{payload.customerId}}',
350
+ },
351
+ {
352
+ name: 'note',
353
+ label: 'Note',
354
+ type: 'string',
355
+ description: 'Internal note, visible to staff in the Shopify admin.',
356
+ },
357
+ {
358
+ name: 'tags',
359
+ label: 'Tags',
360
+ type: 'tags',
361
+ description: 'Tags to put on the draft order.',
362
+ },
363
+ {
364
+ name: 'shippingAddress',
365
+ label: 'Shipping address',
366
+ type: 'json',
367
+ description: 'A JSON object: {"address1":"…","city":"…","province":"…","country":"…","zip":"…"}. Leave empty to let the merchant fill it in.',
368
+ },
369
+ ],
370
+ },
371
+ completeDraftOrder: {
372
+ label: 'Complete draft order',
373
+ description: 'Turn a draft order into a real order. This is the step that reserves stock and can charge — it is not undone by running it again.',
374
+ apiRoute: 'mutation draftOrderComplete',
375
+ params: [
376
+ {
377
+ name: 'draftOrderId',
378
+ label: 'Draft order id',
379
+ type: 'gid',
380
+ required: true,
381
+ description: 'gid://shopify/DraftOrder/1234. The create-draft operation puts one in its output, so the usual shape is one node feeding the next.',
382
+ placeholder: '{{payload.id}}',
383
+ },
384
+ {
385
+ name: 'paymentGatewayId',
386
+ label: 'Payment gateway id',
387
+ type: 'gid',
388
+ description: 'Which gateway processes it. Leave empty to complete the order as pending payment, which is what most automations want.',
389
+ },
390
+ {
391
+ name: 'sourceName',
392
+ label: 'Source name',
393
+ type: 'string',
394
+ description: 'A sales-channel handle, for attribution in the store\'s reports.',
395
+ },
396
+ ],
397
+ },
398
+ fulfillOrder: {
399
+ label: 'Fulfil order',
400
+ description: "Mark an order shipped, optionally with tracking. Fulfils every one of the order's open fulfilment orders.",
401
+ apiRoute: 'query order.fulfillmentOrders, then mutation fulfillmentCreate (one call per order, all its fulfilment orders in one)',
402
+ params: [
403
+ {
404
+ name: 'orderId',
405
+ label: 'Order id',
406
+ type: 'gid',
407
+ required: true,
408
+ description: 'gid://shopify/Order/1234. NOT a fulfilment order id — those are looked up from this one, because Shopify has no "fulfil this order" mutation.',
409
+ placeholder: '{{payload.admin_graphql_api_id}}',
410
+ },
411
+ {
412
+ name: 'trackingNumber',
413
+ label: 'Tracking number',
414
+ type: 'string',
415
+ description: 'Optional. With a number and no carrier, Shopify tries to guess the carrier from the number.',
416
+ placeholder: '{{payload.tracking_number}}',
417
+ },
418
+ {
419
+ name: 'trackingCompany',
420
+ label: 'Carrier',
421
+ type: 'string',
422
+ description: 'Optional. Shopify builds the tracking link itself for carriers it knows by name.',
423
+ placeholder: 'DHL Express',
424
+ },
425
+ {
426
+ name: 'trackingUrl',
427
+ label: 'Tracking URL',
428
+ type: 'string',
429
+ description: 'Optional. Only needed for a carrier Shopify does not know — otherwise the number is enough.',
430
+ },
431
+ {
432
+ name: 'notifyCustomer',
433
+ label: 'Email the customer',
434
+ type: 'boolean',
435
+ description: 'Off by default. On sends the shipping confirmation, which is a real email to a real person — the one field here with a consequence outside the store.',
436
+ },
437
+ ],
438
+ },
439
+ cancelOrder: {
440
+ label: 'Cancel order',
441
+ description: 'Cancel an order, optionally restocking it and refunding. Shopify runs this as a background job, so success means "accepted", not "already done".',
442
+ apiRoute: 'mutation orderCancel',
443
+ params: [
444
+ {
445
+ name: 'orderId',
446
+ label: 'Order id',
447
+ type: 'gid',
448
+ required: true,
449
+ description: 'gid://shopify/Order/1234.',
450
+ placeholder: '{{payload.admin_graphql_api_id}}',
451
+ },
452
+ {
453
+ name: 'reason',
454
+ label: 'Reason',
455
+ type: 'cancelReason',
456
+ required: true,
457
+ description: "Required by Shopify, not by us, and it lands on the order where the merchant reads it. OTHER is the honest answer when none of the others fit.",
458
+ },
459
+ {
460
+ name: 'restock',
461
+ label: 'Put the stock back',
462
+ type: 'boolean',
463
+ description: 'Off by default, which is the safe side: restocking something that never left is how a shelf count goes wrong. Turn it on when the goods really are still there.',
464
+ },
465
+ {
466
+ name: 'notifyCustomer',
467
+ label: 'Email the customer',
468
+ type: 'boolean',
469
+ description: 'Off by default. On sends the cancellation email.',
470
+ },
471
+ {
472
+ name: 'staffNote',
473
+ label: 'Staff note',
474
+ type: 'string',
475
+ description: 'Internal note on the cancellation, for whoever reads it later.',
476
+ },
477
+ ],
478
+ },
479
+ setMetafield: {
480
+ label: 'Set a metafield',
481
+ description: 'Write one piece of custom data onto an order, a customer, a product or a variant — the general way to keep a foreign id or a computed value on a Shopify record.',
482
+ apiRoute: 'mutation metafieldsSet',
483
+ params: [
484
+ {
485
+ name: 'ownerId',
486
+ label: 'Owner id',
487
+ type: 'gid',
488
+ required: true,
489
+ description: 'What the metafield hangs off, as a global id — an order, a customer, a product, a variant. A Shopify trigger gives you one in the payload.',
490
+ placeholder: '{{payload.admin_graphql_api_id}}',
491
+ },
492
+ {
493
+ name: 'namespace',
494
+ label: 'Namespace',
495
+ type: 'string',
496
+ required: true,
497
+ description: '"custom" is the namespace the Shopify admin shows merchants; anything else is only visible through the API unless a definition exists for it.',
498
+ placeholder: 'custom',
499
+ },
500
+ {
501
+ name: 'key',
502
+ label: 'Key',
503
+ type: 'string',
504
+ required: true,
505
+ description: 'The field name inside the namespace.',
506
+ placeholder: 'external_id',
507
+ },
508
+ {
509
+ name: 'type',
510
+ label: 'Type',
511
+ type: 'string',
512
+ required: true,
513
+ description: 'The metafield type, e.g. single_line_text_field, number_integer, boolean, json, date. It is NOT inferred from the value — the wrong type is rejected, which is better than silently storing a number as text.',
514
+ placeholder: 'single_line_text_field',
515
+ },
516
+ {
517
+ name: 'value',
518
+ label: 'Value',
519
+ type: 'string',
520
+ required: true,
521
+ description: 'Always sent as a string, whatever the type says: Shopify parses it against the type on its side. Setting it again REPLACES what was there.',
522
+ placeholder: '{{payload.id}}',
523
+ },
524
+ ],
525
+ },
526
+ createProduct: {
527
+ label: 'Create product',
528
+ description: 'Put a product in the catalogue. Creates the product and its one default variant; several variants, prices and media are separate mutations and are not offered here.',
529
+ apiRoute: 'mutation productCreate',
530
+ params: [
531
+ {
532
+ name: 'title',
533
+ label: 'Title',
534
+ type: 'string',
535
+ required: true,
536
+ description: 'The only field Shopify insists on.',
537
+ placeholder: '{{payload.title}}',
538
+ },
539
+ {
540
+ name: 'descriptionHtml',
541
+ label: 'Description',
542
+ type: 'string',
543
+ description: 'HTML, not plain text — line breaks typed here do not survive as line breaks in the storefront.',
544
+ },
545
+ {
546
+ name: 'vendor',
547
+ label: 'Vendor',
548
+ type: 'string',
549
+ description: 'Free text; Shopify does not check it against anything.',
550
+ },
551
+ {
552
+ name: 'productType',
553
+ label: 'Product type',
554
+ type: 'string',
555
+ description: 'Free text, and separate from the store\'s collections.',
556
+ },
557
+ {
558
+ name: 'tags',
559
+ label: 'Tags',
560
+ type: 'tags',
561
+ description: 'Tags to put on the product.',
562
+ },
563
+ {
564
+ name: 'status',
565
+ label: 'Status',
566
+ type: 'productStatus',
567
+ description: 'Left empty, Shopify makes it ACTIVE — visible to shoppers the moment it is created. Pick DRAFT when a person should look at it first.',
568
+ },
569
+ {
570
+ name: 'handle',
571
+ label: 'Handle',
572
+ type: 'string',
573
+ description: 'The URL slug. Left empty, Shopify builds one from the title; set it and a collision fails the whole mutation.',
574
+ },
575
+ ],
576
+ },
263
577
  };
578
+ /**
579
+ * The OAuth scope each operation needs, so a missing one is named BEFORE the
580
+ * call instead of arriving as an HTTP 403 on a request that looks fine.
581
+ *
582
+ * ## The list is "any one of", not "all of"
583
+ *
584
+ * That is `fulfillOrder`'s doing and it is not a generalisation for its own
585
+ * sake: which fulfilment-order scope applies depends on where the order is
586
+ * fulfilled from — the merchant's own locations, a third-party service, or the
587
+ * app itself acting as one. Shopify accepts the mutation if the token carries
588
+ * ANY of the three. Requiring all three would refuse a store that is correctly
589
+ * set up.
590
+ *
591
+ * ## An empty list means "not checkable from the operation alone"
592
+ *
593
+ * `setTags` tags an order, a customer or a product through the same mutation,
594
+ * and `setMetafield` writes onto whichever resource the owner id points at —
595
+ * so the scope depends on a FIELD, not on the operation. Guessing would be
596
+ * worse than not checking: a wrong guess blocks a call Shopify would have
597
+ * accepted, and a pre-flight check that produces false refusals gets deleted.
598
+ * Those fall through to Shopify's own 403, which `describeShopifyError`
599
+ * already explains.
600
+ *
601
+ * ⚠️ Read against `shopify.dev/docs/api/usage/access-scopes` and each
602
+ * mutation's own "Access requirements". Two things follow from that page and
603
+ * are relied on by `scopesQueFaltanEnShopify`:
604
+ *
605
+ * - **A write scope includes read.** `write_orders` grants `read_orders`, so
606
+ * a required `read_x` is satisfied by a granted `write_x`.
607
+ * - **Scopes are granted at install.** Adding one to the app does not give
608
+ * it to credentials that already exist; those have to be reconnected.
609
+ */
610
+ exports.SHOPIFY_OPERATION_SCOPES = {
611
+ upsertCustomer: ['write_customers'],
612
+ /* Depende del recurso elegido, no de la operación. Ver la cabecera. */
613
+ setTags: [],
614
+ adjustInventory: ['write_inventory'],
615
+ /* `findRecords` lee tres colecciones distintas según el recurso. Igual. */
616
+ findRecords: [],
617
+ createDraftOrder: ['write_draft_orders'],
618
+ completeDraftOrder: ['write_draft_orders'],
619
+ fulfillOrder: [
620
+ 'write_merchant_managed_fulfillment_orders',
621
+ 'write_third_party_fulfillment_orders',
622
+ 'write_assigned_fulfillment_orders',
623
+ ],
624
+ cancelOrder: ['write_orders'],
625
+ /* Depende del dueño al que apunte `ownerId`. Ver la cabecera. */
626
+ setMetafield: [],
627
+ createProduct: ['write_products'],
628
+ };
629
+ /**
630
+ * Which of an operation's scopes the credential does NOT have.
631
+ *
632
+ * Empty means "nothing to say" — and it says that in three different
633
+ * situations, all of which have to fail OPEN:
634
+ *
635
+ * 1. The operation declares no scope (the resource-dependent ones).
636
+ * 2. The credential has a scope string and it covers one of the alternatives.
637
+ * 3. **The credential has no scope string at all.** Shopify returns the
638
+ * granted scopes on the token exchange, but a credential stored before
639
+ * that was read, or one whose metadata was pruned, has an empty string —
640
+ * and refusing to run a node because we cannot see its permissions would
641
+ * break working flows to prevent a maybe. Shopify is the authority here;
642
+ * this check only saves the round trip when it can prove the answer.
643
+ *
644
+ * @param concedidos what Shopify granted, as it sends it: a comma-separated
645
+ * string, or the already-split list.
646
+ */
647
+ function scopesQueFaltanEnShopify(concedidos, requeridos) {
648
+ if (requeridos.length === 0)
649
+ return [];
650
+ const lista = (typeof concedidos === 'string'
651
+ ? concedidos.split(',')
652
+ : Array.isArray(concedidos)
653
+ ? concedidos
654
+ : [])
655
+ .map((s) => String(s).trim())
656
+ .filter(Boolean);
657
+ // Fail open: sin scopes a la vista no se puede probar que falte ninguno.
658
+ if (lista.length === 0)
659
+ return [];
660
+ const tiene = new Set(lista);
661
+ const cubierto = (scope) => tiene.has(scope) ||
662
+ // `write_x` incluye `read_x`, y ésa es la única implicación que existe.
663
+ (scope.startsWith('read_') && tiene.has(`write_${scope.slice(5)}`));
664
+ // «Cualquiera de» — con una basta, y entonces no falta nada.
665
+ if (requeridos.some(cubierto))
666
+ return [];
667
+ return [...requeridos];
668
+ }