@keystrokehq/shopify 0.0.1

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 (46) hide show
  1. package/README.md +178 -0
  2. package/dist/_official/index.d.mts +3 -0
  3. package/dist/_official/index.mjs +3 -0
  4. package/dist/_runtime/index.d.mts +56 -0
  5. package/dist/_runtime/index.mjs +3 -0
  6. package/dist/articles.d.mts +174 -0
  7. package/dist/articles.mjs +252 -0
  8. package/dist/blogs.d.mts +113 -0
  9. package/dist/blogs.mjs +174 -0
  10. package/dist/client.d.mts +22 -0
  11. package/dist/client.mjs +90 -0
  12. package/dist/collections.d.mts +140 -0
  13. package/dist/collections.mjs +209 -0
  14. package/dist/connection.d.mts +2 -0
  15. package/dist/connection.mjs +3 -0
  16. package/dist/customers.d.mts +161 -0
  17. package/dist/customers.mjs +225 -0
  18. package/dist/events.d.mts +181 -0
  19. package/dist/events.mjs +67 -0
  20. package/dist/factory-DC-gY8L0.mjs +8 -0
  21. package/dist/fulfillments.d.mts +99 -0
  22. package/dist/fulfillments.mjs +167 -0
  23. package/dist/index.d.mts +1 -0
  24. package/dist/index.mjs +1 -0
  25. package/dist/integration-BN3pjCz4.mjs +204 -0
  26. package/dist/integration-BwDBsGX-.d.mts +71 -0
  27. package/dist/inventory.d.mts +103 -0
  28. package/dist/inventory.mjs +182 -0
  29. package/dist/locations.d.mts +47 -0
  30. package/dist/locations.mjs +81 -0
  31. package/dist/messaging.d.mts +1 -0
  32. package/dist/messaging.mjs +1 -0
  33. package/dist/operation-helpers-CKEDIx0o.mjs +21 -0
  34. package/dist/orders.d.mts +229 -0
  35. package/dist/orders.mjs +271 -0
  36. package/dist/products.d.mts +155 -0
  37. package/dist/products.mjs +171 -0
  38. package/dist/provider-app-DHxVZI6q.d.mts +37 -0
  39. package/dist/schemas.d.mts +233 -0
  40. package/dist/schemas.mjs +164 -0
  41. package/dist/shop.d.mts +20 -0
  42. package/dist/shop.mjs +31 -0
  43. package/dist/triggers.d.mts +23 -0
  44. package/dist/triggers.mjs +138 -0
  45. package/dist/webhook-management-BmIxO0vr.mjs +150 -0
  46. package/package.json +130 -0
@@ -0,0 +1,138 @@
1
+ import { a as shopifyAppCredentialSet, t as shopify } from "./integration-BN3pjCz4.mjs";
2
+ import { shopifyOrderCancelledEventSchema, shopifyOrderCreatedEventSchema, shopifyOrderFulfilledEventSchema, shopifyOrderUpdatedEventSchema, shopifyOrderWebhookPayloadSchema, shopifyProductCreatedEventSchema, shopifyProductDeletedEventSchema, shopifyProductUpdatedEventSchema, shopifyProductWebhookPayloadSchema } from "./events.mjs";
3
+ import { t as SHOPIFY_WEBHOOK_RUNTIME_DEFINITIONS } from "./webhook-management-BmIxO0vr.mjs";
4
+ import { createHmac, timingSafeEqual } from "node:crypto";
5
+ import { createWebhookTriggerBindingFactory } from "@keystrokehq/integration-authoring";
6
+
7
+ //#region src/verification.ts
8
+ function getHeader(headers, name) {
9
+ const normalizedName = name.toLowerCase();
10
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === normalizedName) return value;
11
+ }
12
+ function verifyShopifyWebhook(request, webhookSecret) {
13
+ const signature = getHeader(request.headers, "x-shopify-hmac-sha256");
14
+ if (!signature) return false;
15
+ const expectedDigest = createHmac("sha256", webhookSecret).update(request.rawBody, "utf8").digest("base64");
16
+ const signatureBuffer = Buffer.from(signature, "utf8");
17
+ const expectedBuffer = Buffer.from(expectedDigest, "utf8");
18
+ if (signatureBuffer.length !== expectedBuffer.length) return false;
19
+ return timingSafeEqual(signatureBuffer, expectedBuffer);
20
+ }
21
+
22
+ //#endregion
23
+ //#region src/triggers.ts
24
+ function getWebhookPath(topic) {
25
+ const definition = SHOPIFY_WEBHOOK_RUNTIME_DEFINITIONS.find((candidate) => candidate.eventTopic === topic);
26
+ if (!definition) throw new Error(`Unsupported Shopify webhook topic: ${topic}`);
27
+ return definition.path;
28
+ }
29
+ function extractStringField(payload, fieldNames) {
30
+ for (const fieldName of fieldNames) {
31
+ const value = payload[fieldName];
32
+ if (typeof value === "string" && value.length > 0) return value;
33
+ }
34
+ }
35
+ function extractNumberField(payload, fieldNames) {
36
+ for (const fieldName of fieldNames) {
37
+ const value = payload[fieldName];
38
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
39
+ }
40
+ }
41
+ function buildShopifyWebhookEventId(topic, payload) {
42
+ return `${topic}:${extractStringField(payload, ["admin_graphql_api_id"]) ?? extractNumberField(payload, ["id"]) ?? "unknown-resource"}:${extractStringField(payload, [
43
+ "updated_at",
44
+ "created_at",
45
+ "cancelled_at",
46
+ "processed_at",
47
+ "published_at"
48
+ ]) ?? "unknown-time"}`;
49
+ }
50
+ function createShopifyWebhookBindingFactory(config) {
51
+ return createWebhookTriggerBindingFactory({
52
+ defaultName: config.defaultName,
53
+ defaultDescription: config.defaultDescription,
54
+ path: getWebhookPath(config.topic),
55
+ method: "POST",
56
+ payload: config.payloadSchema,
57
+ credentialSets: [shopify, shopifyAppCredentialSet],
58
+ verify: (request, ctx) => {
59
+ const appCredentials = ctx.credentials["shopify-app"];
60
+ if (!verifyShopifyWebhook(request, appCredentials.webhookSecret)) throw new Error("Invalid Shopify webhook signature.");
61
+ },
62
+ mapPayload: (payload) => config.eventSchema.parse({
63
+ eventId: buildShopifyWebhookEventId(config.topic, payload),
64
+ topic: config.topic,
65
+ resourceId: extractStringField(payload, ["admin_graphql_api_id"]) ?? extractNumberField(payload, ["id"]),
66
+ occurredAt: extractStringField(payload, [
67
+ "updated_at",
68
+ "created_at",
69
+ "cancelled_at",
70
+ "processed_at",
71
+ "published_at"
72
+ ]),
73
+ payload
74
+ }),
75
+ response: { successStatus: 200 }
76
+ });
77
+ }
78
+ const productCreatedBinding = createShopifyWebhookBindingFactory({
79
+ topic: "products/create",
80
+ defaultName: "Shopify Product Created",
81
+ defaultDescription: "Trigger when a Shopify product is created.",
82
+ payloadSchema: shopifyProductWebhookPayloadSchema,
83
+ eventSchema: shopifyProductCreatedEventSchema
84
+ });
85
+ const productUpdatedBinding = createShopifyWebhookBindingFactory({
86
+ topic: "products/update",
87
+ defaultName: "Shopify Product Updated",
88
+ defaultDescription: "Trigger when a Shopify product is updated.",
89
+ payloadSchema: shopifyProductWebhookPayloadSchema,
90
+ eventSchema: shopifyProductUpdatedEventSchema
91
+ });
92
+ const productDeletedBinding = createShopifyWebhookBindingFactory({
93
+ topic: "products/delete",
94
+ defaultName: "Shopify Product Deleted",
95
+ defaultDescription: "Trigger when a Shopify product is deleted.",
96
+ payloadSchema: shopifyProductWebhookPayloadSchema,
97
+ eventSchema: shopifyProductDeletedEventSchema
98
+ });
99
+ const orderCreatedBinding = createShopifyWebhookBindingFactory({
100
+ topic: "orders/create",
101
+ defaultName: "Shopify Order Created",
102
+ defaultDescription: "Trigger when a Shopify order is created.",
103
+ payloadSchema: shopifyOrderWebhookPayloadSchema,
104
+ eventSchema: shopifyOrderCreatedEventSchema
105
+ });
106
+ const orderUpdatedBinding = createShopifyWebhookBindingFactory({
107
+ topic: "orders/updated",
108
+ defaultName: "Shopify Order Updated",
109
+ defaultDescription: "Trigger when a Shopify order is updated.",
110
+ payloadSchema: shopifyOrderWebhookPayloadSchema,
111
+ eventSchema: shopifyOrderUpdatedEventSchema
112
+ });
113
+ const orderCancelledBinding = createShopifyWebhookBindingFactory({
114
+ topic: "orders/cancelled",
115
+ defaultName: "Shopify Order Cancelled",
116
+ defaultDescription: "Trigger when a Shopify order is cancelled.",
117
+ payloadSchema: shopifyOrderWebhookPayloadSchema,
118
+ eventSchema: shopifyOrderCancelledEventSchema
119
+ });
120
+ const orderFulfilledBinding = createShopifyWebhookBindingFactory({
121
+ topic: "orders/fulfilled",
122
+ defaultName: "Shopify Order Fulfilled",
123
+ defaultDescription: "Trigger when a Shopify order is fulfilled.",
124
+ payloadSchema: shopifyOrderWebhookPayloadSchema,
125
+ eventSchema: shopifyOrderFulfilledEventSchema
126
+ });
127
+ const webhooks = Object.freeze({
128
+ productCreated: (options) => productCreatedBinding(options),
129
+ productUpdated: (options) => productUpdatedBinding(options),
130
+ productDeleted: (options) => productDeletedBinding(options),
131
+ orderCreated: (options) => orderCreatedBinding(options),
132
+ orderUpdated: (options) => orderUpdatedBinding(options),
133
+ orderCancelled: (options) => orderCancelledBinding(options),
134
+ orderFulfilled: (options) => orderFulfilledBinding(options)
135
+ });
136
+
137
+ //#endregion
138
+ export { webhooks };
@@ -0,0 +1,150 @@
1
+ import { shopifyGlobalIdSchema, shopifyUserErrorSchema } from "./schemas.mjs";
2
+ import { createShopifyClient } from "./client.mjs";
3
+ import { shopifyWebhookSubscriptionTopicSchema, shopifyWebhookTopicSchema } from "./events.mjs";
4
+ import { z } from "zod";
5
+
6
+ //#region src/webhook-management.ts
7
+ const shopifyWebhookRuntimeDefinitionSchema = z.object({
8
+ eventTopic: shopifyWebhookTopicSchema,
9
+ subscriptionTopic: shopifyWebhookSubscriptionTopicSchema,
10
+ path: z.string().min(1)
11
+ });
12
+ const SHOPIFY_WEBHOOK_RUNTIME_DEFINITIONS = [
13
+ {
14
+ eventTopic: "products/create",
15
+ subscriptionTopic: "PRODUCTS_CREATE",
16
+ path: "/shopify/products/create"
17
+ },
18
+ {
19
+ eventTopic: "products/update",
20
+ subscriptionTopic: "PRODUCTS_UPDATE",
21
+ path: "/shopify/products/update"
22
+ },
23
+ {
24
+ eventTopic: "products/delete",
25
+ subscriptionTopic: "PRODUCTS_DELETE",
26
+ path: "/shopify/products/delete"
27
+ },
28
+ {
29
+ eventTopic: "orders/create",
30
+ subscriptionTopic: "ORDERS_CREATE",
31
+ path: "/shopify/orders/create"
32
+ },
33
+ {
34
+ eventTopic: "orders/updated",
35
+ subscriptionTopic: "ORDERS_UPDATED",
36
+ path: "/shopify/orders/updated"
37
+ },
38
+ {
39
+ eventTopic: "orders/cancelled",
40
+ subscriptionTopic: "ORDERS_CANCELLED",
41
+ path: "/shopify/orders/cancelled"
42
+ },
43
+ {
44
+ eventTopic: "orders/fulfilled",
45
+ subscriptionTopic: "ORDERS_FULFILLED",
46
+ path: "/shopify/orders/fulfilled"
47
+ }
48
+ ];
49
+ const webhookSubscriptionSchema = z.object({
50
+ id: shopifyGlobalIdSchema,
51
+ topic: shopifyWebhookSubscriptionTopicSchema,
52
+ uri: z.string().url()
53
+ });
54
+ const listWebhookSubscriptionsResponseSchema = z.object({ webhookSubscriptions: z.object({ nodes: z.array(webhookSubscriptionSchema) }) });
55
+ const createWebhookSubscriptionResponseSchema = z.object({ webhookSubscriptionCreate: z.object({
56
+ webhookSubscription: webhookSubscriptionSchema.nullable(),
57
+ userErrors: z.array(shopifyUserErrorSchema)
58
+ }) });
59
+ const deleteWebhookSubscriptionResponseSchema = z.object({ webhookSubscriptionDelete: z.object({
60
+ deletedWebhookSubscriptionId: shopifyGlobalIdSchema.nullable(),
61
+ userErrors: z.array(shopifyUserErrorSchema)
62
+ }) });
63
+ function assertNoUserErrors(userErrors) {
64
+ if (userErrors.length === 0) return;
65
+ const message = userErrors.map((error) => error.field && error.field.length > 0 ? `${error.field.join(".")}: ${error.message}` : error.message).join("; ");
66
+ throw new Error(`Shopify webhook subscription error: ${message}`);
67
+ }
68
+ function buildShopifyWebhookCallbackUri(baseUrl, path) {
69
+ return new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
70
+ }
71
+ async function listShopifyWebhookSubscriptions(credentials) {
72
+ return (await createShopifyClient(credentials).graphql(`
73
+ query ListWebhookSubscriptions($topics: [WebhookSubscriptionTopic!]) {
74
+ webhookSubscriptions(first: 50, topics: $topics) {
75
+ nodes {
76
+ id
77
+ topic
78
+ uri
79
+ }
80
+ }
81
+ }
82
+ `, listWebhookSubscriptionsResponseSchema, { variables: { topics: SHOPIFY_WEBHOOK_RUNTIME_DEFINITIONS.map((definition) => definition.subscriptionTopic) } })).webhookSubscriptions.nodes;
83
+ }
84
+ async function createShopifyWebhookSubscription(credentials, topic, uri) {
85
+ const response = await createShopifyClient(credentials).graphql(`
86
+ mutation CreateWebhookSubscription(
87
+ $topic: WebhookSubscriptionTopic!
88
+ $webhookSubscription: WebhookSubscriptionInput!
89
+ ) {
90
+ webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
91
+ webhookSubscription {
92
+ id
93
+ topic
94
+ uri
95
+ }
96
+ userErrors {
97
+ field
98
+ message
99
+ }
100
+ }
101
+ }
102
+ `, createWebhookSubscriptionResponseSchema, { variables: {
103
+ topic,
104
+ webhookSubscription: { uri }
105
+ } });
106
+ assertNoUserErrors(response.webhookSubscriptionCreate.userErrors);
107
+ if (!response.webhookSubscriptionCreate.webhookSubscription) throw new Error(`Shopify did not return the created webhook subscription for ${topic}.`);
108
+ return response.webhookSubscriptionCreate.webhookSubscription;
109
+ }
110
+ async function deleteShopifyWebhookSubscription(credentials, id) {
111
+ const response = await createShopifyClient(credentials).graphql(`
112
+ mutation DeleteWebhookSubscription($id: ID!) {
113
+ webhookSubscriptionDelete(id: $id) {
114
+ deletedWebhookSubscriptionId
115
+ userErrors {
116
+ field
117
+ message
118
+ }
119
+ }
120
+ }
121
+ `, deleteWebhookSubscriptionResponseSchema, { variables: { id } });
122
+ assertNoUserErrors(response.webhookSubscriptionDelete.userErrors);
123
+ if (!response.webhookSubscriptionDelete.deletedWebhookSubscriptionId) throw new Error(`Shopify did not confirm deletion for webhook subscription ${id}.`);
124
+ return response.webhookSubscriptionDelete.deletedWebhookSubscriptionId;
125
+ }
126
+ async function ensureShopifyWebhookSubscriptions(credentials, callbackBaseUrl) {
127
+ const existing = await listShopifyWebhookSubscriptions(credentials);
128
+ const kept = [];
129
+ const created = [];
130
+ const deleted = [];
131
+ for (const definition of SHOPIFY_WEBHOOK_RUNTIME_DEFINITIONS) {
132
+ const expectedUri = buildShopifyWebhookCallbackUri(callbackBaseUrl, definition.path);
133
+ const matches = existing.filter((subscription) => subscription.topic === definition.subscriptionTopic);
134
+ const canonical = matches.find((subscription) => subscription.uri === expectedUri);
135
+ if (canonical) kept.push(canonical.id);
136
+ for (const subscription of matches) if (!canonical || subscription.id !== canonical.id) deleted.push(await deleteShopifyWebhookSubscription(credentials, subscription.id));
137
+ if (!canonical) {
138
+ const createdSubscription = await createShopifyWebhookSubscription(credentials, definition.subscriptionTopic, expectedUri);
139
+ created.push(createdSubscription.id);
140
+ }
141
+ }
142
+ return {
143
+ kept,
144
+ created,
145
+ deleted
146
+ };
147
+ }
148
+
149
+ //#endregion
150
+ export { listShopifyWebhookSubscriptions as i, buildShopifyWebhookCallbackUri as n, ensureShopifyWebhookSubscriptions as r, SHOPIFY_WEBHOOK_RUNTIME_DEFINITIONS as t };
package/package.json ADDED
@@ -0,0 +1,130 @@
1
+ {
2
+ "name": "@keystrokehq/shopify",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "sideEffects": false,
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.mts",
10
+ "default": "./dist/index.mjs"
11
+ },
12
+ "./messaging": {
13
+ "types": "./dist/messaging.d.mts",
14
+ "default": "./dist/messaging.mjs"
15
+ },
16
+ "./connection": {
17
+ "types": "./dist/connection.d.mts",
18
+ "default": "./dist/connection.mjs"
19
+ },
20
+ "./client": {
21
+ "types": "./dist/client.d.mts",
22
+ "default": "./dist/client.mjs"
23
+ },
24
+ "./schemas": {
25
+ "types": "./dist/schemas.d.mts",
26
+ "default": "./dist/schemas.mjs"
27
+ },
28
+ "./events": {
29
+ "types": "./dist/events.d.mts",
30
+ "default": "./dist/events.mjs"
31
+ },
32
+ "./shop": {
33
+ "types": "./dist/shop.d.mts",
34
+ "default": "./dist/shop.mjs"
35
+ },
36
+ "./products": {
37
+ "types": "./dist/products.d.mts",
38
+ "default": "./dist/products.mjs"
39
+ },
40
+ "./collections": {
41
+ "types": "./dist/collections.d.mts",
42
+ "default": "./dist/collections.mjs"
43
+ },
44
+ "./orders": {
45
+ "types": "./dist/orders.d.mts",
46
+ "default": "./dist/orders.mjs"
47
+ },
48
+ "./customers": {
49
+ "types": "./dist/customers.d.mts",
50
+ "default": "./dist/customers.mjs"
51
+ },
52
+ "./blogs": {
53
+ "types": "./dist/blogs.d.mts",
54
+ "default": "./dist/blogs.mjs"
55
+ },
56
+ "./articles": {
57
+ "types": "./dist/articles.d.mts",
58
+ "default": "./dist/articles.mjs"
59
+ },
60
+ "./inventory": {
61
+ "types": "./dist/inventory.d.mts",
62
+ "default": "./dist/inventory.mjs"
63
+ },
64
+ "./locations": {
65
+ "types": "./dist/locations.d.mts",
66
+ "default": "./dist/locations.mjs"
67
+ },
68
+ "./fulfillments": {
69
+ "types": "./dist/fulfillments.d.mts",
70
+ "default": "./dist/fulfillments.mjs"
71
+ },
72
+ "./triggers": {
73
+ "types": "./dist/triggers.d.mts",
74
+ "default": "./dist/triggers.mjs"
75
+ },
76
+ "./_official": {
77
+ "types": "./dist/_official/index.d.mts",
78
+ "default": "./dist/_official/index.mjs"
79
+ },
80
+ "./_runtime": {
81
+ "types": "./dist/_runtime/index.d.mts",
82
+ "default": "./dist/_runtime/index.mjs"
83
+ }
84
+ },
85
+ "files": [
86
+ "dist",
87
+ "README.md",
88
+ "LICENSE"
89
+ ],
90
+ "scripts": {
91
+ "typecheck": "tsgo --build",
92
+ "build": "tsdown",
93
+ "lint": "biome check .",
94
+ "test:unit": "vitest run --passWithNoTests --project unit",
95
+ "test:int": "vitest run --passWithNoTests --project int",
96
+ "prepublishOnly": "pnpm build && pnpm test:unit",
97
+ "lint:fix": "biome check --write ."
98
+ },
99
+ "dependencies": {
100
+ "@shopify/admin-api-client": "^1.1.2",
101
+ "@keystrokehq/credential-connection": "^0.0.1",
102
+ "@keystrokehq/integration-authoring": "^0.0.1",
103
+ "@keystrokehq/core": "^0.0.5",
104
+ "zod": "^4.3.6"
105
+ },
106
+ "devDependencies": {
107
+ "@types/node": "catalog:",
108
+ "@keystrokehq/test-utils": "workspace:*",
109
+ "@keystrokehq/typescript-config": "workspace:*",
110
+ "tsdown": "catalog:",
111
+ "typescript": "catalog:",
112
+ "vitest": "catalog:"
113
+ },
114
+ "keywords": [
115
+ "shopify",
116
+ "keystroke",
117
+ "integration",
118
+ "commerce"
119
+ ],
120
+ "repository": {
121
+ "type": "git",
122
+ "url": "https://github.com/keystrokehq/integrations",
123
+ "directory": "integrations/shopify"
124
+ },
125
+ "license": "MIT",
126
+ "publishConfig": {
127
+ "access": "public",
128
+ "registry": "https://registry.npmjs.org/"
129
+ }
130
+ }