@keystrokehq/shopify 0.0.11 → 0.0.15
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/_official/index.d.mts +3 -8
- package/dist/_official/index.mjs +1 -1
- package/dist/_runtime/index.d.mts +1 -1
- package/dist/articles.d.mts +11 -27
- package/dist/articles.mjs +1 -1
- package/dist/blogs.d.mts +11 -27
- package/dist/blogs.mjs +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/collections.d.mts +13 -32
- package/dist/collections.mjs +1 -1
- package/dist/connection.d.mts +1 -1
- package/dist/connection.mjs +1 -1
- package/dist/customers.d.mts +13 -32
- package/dist/customers.mjs +1 -1
- package/dist/factory-DwZJl9JC.mjs +8 -0
- package/dist/fulfillments.d.mts +7 -17
- package/dist/fulfillments.mjs +1 -1
- package/dist/integration-B5LK_0eR.mjs +63 -0
- package/dist/integration-DrHCkXco.d.mts +27 -0
- package/dist/inventory.d.mts +9 -22
- package/dist/inventory.mjs +1 -1
- package/dist/locations.d.mts +5 -12
- package/dist/locations.mjs +1 -1
- package/dist/orders.d.mts +11 -27
- package/dist/orders.mjs +1 -1
- package/dist/products.d.mts +11 -27
- package/dist/products.mjs +1 -1
- package/dist/schemas.mjs +1 -1
- package/dist/shop.d.mts +3 -7
- package/dist/shop.mjs +1 -1
- package/package.json +5 -6
- package/dist/factory-BKmxl2-8.mjs +0 -7
- package/dist/integration-HrSKAMfF.d.mts +0 -70
- package/dist/integration-vSl3XHTG.mjs +0 -340
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { CredentialSet, Operation } from "@keystrokehq/core";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4
|
-
|
|
5
|
-
//#region ../../packages/integration-authoring/dist/official/runtime.mjs
|
|
6
|
-
const REGISTRY_KEY = "__ks_official_integration_metadata_registry";
|
|
7
|
-
function getRegistry() {
|
|
8
|
-
const globalStore = globalThis;
|
|
9
|
-
if (!globalStore[REGISTRY_KEY]) globalStore[REGISTRY_KEY] = /* @__PURE__ */ new WeakMap();
|
|
10
|
-
return globalStore[REGISTRY_KEY];
|
|
11
|
-
}
|
|
12
|
-
function registerOfficialOperation(operation, metadata) {
|
|
13
|
-
getRegistry().set(operation, Object.freeze({ ...metadata }));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region ../../packages/integration-authoring/dist/official/index.mjs
|
|
18
|
-
const OFFICIAL_CREDENTIAL_SET_ID_PREFIX = "keystroke:";
|
|
19
|
-
function officialCredentialSetId(id) {
|
|
20
|
-
return `${OFFICIAL_CREDENTIAL_SET_ID_PREFIX}${id}`;
|
|
21
|
-
}
|
|
22
|
-
function stripOfficialCredentialSetIdPrefix(id) {
|
|
23
|
-
return id.startsWith(OFFICIAL_CREDENTIAL_SET_ID_PREFIX) ? id.slice(10) : id;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Creates a factory for Keystroke-official integration operations.
|
|
27
|
-
*
|
|
28
|
-
* It keeps the same flat `run(input, credentials)` ergonomics as the generic
|
|
29
|
-
* operation factory, while registering official metadata for builder/runtime
|
|
30
|
-
* operation metadata.
|
|
31
|
-
*/
|
|
32
|
-
function createOfficialOperationFactory(credentialSet) {
|
|
33
|
-
const integrationId = stripOfficialCredentialSetIdPrefix(credentialSet.id);
|
|
34
|
-
return (config) => {
|
|
35
|
-
const wrappedRun = async (input, ctx) => {
|
|
36
|
-
const creds = ctx.credentials[credentialSet.id];
|
|
37
|
-
return config.run(input, creds);
|
|
38
|
-
};
|
|
39
|
-
const operation = new Operation({
|
|
40
|
-
id: config.id,
|
|
41
|
-
name: config.name,
|
|
42
|
-
description: config.description,
|
|
43
|
-
input: config.input,
|
|
44
|
-
output: config.output,
|
|
45
|
-
credentialSets: [credentialSet],
|
|
46
|
-
...config.tags !== void 0 ? { tags: config.tags } : {},
|
|
47
|
-
...config.needsApproval !== void 0 ? { needsApproval: config.needsApproval } : {},
|
|
48
|
-
...config.requiredOAuthScopes !== void 0 ? { requiredOAuthScopes: config.requiredOAuthScopes } : {},
|
|
49
|
-
...config.retries !== void 0 ? { retries: config.retries } : {},
|
|
50
|
-
...config.timeout !== void 0 ? { timeout: config.timeout } : {},
|
|
51
|
-
...config.maxDurationMs !== void 0 ? { maxDurationMs: config.maxDurationMs } : {},
|
|
52
|
-
run: wrappedRun
|
|
53
|
-
});
|
|
54
|
-
registerOfficialOperation(operation, { integrationId });
|
|
55
|
-
return operation;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Creates an official integration bundle from a single config object.
|
|
60
|
-
*
|
|
61
|
-
* - Creates the public `CredentialSet` internally.
|
|
62
|
-
* - Accepts optional `internal` fields for Keystroke-owned platform credentials.
|
|
63
|
-
*/
|
|
64
|
-
function defineOfficialIntegration(config) {
|
|
65
|
-
const internalCredentialSets = config.internal ?? {};
|
|
66
|
-
const allInternalCredentialSets = [
|
|
67
|
-
...internalCredentialSets.providerApp ? [internalCredentialSets.providerApp] : [],
|
|
68
|
-
...internalCredentialSets.providerAppVariants ?? [],
|
|
69
|
-
...internalCredentialSets.webhookVerification ? [internalCredentialSets.webhookVerification] : [],
|
|
70
|
-
...internalCredentialSets.other ?? []
|
|
71
|
-
];
|
|
72
|
-
const credentialSet = new CredentialSet({
|
|
73
|
-
id: config.id,
|
|
74
|
-
name: config.name,
|
|
75
|
-
description: config.description,
|
|
76
|
-
auth: config.auth,
|
|
77
|
-
...config.connections ? { connections: config.connections } : {},
|
|
78
|
-
...config.proxy ? { proxy: config.proxy } : {},
|
|
79
|
-
...config.needsRawSecret === true ? { needsRawSecret: true } : {}
|
|
80
|
-
});
|
|
81
|
-
return Object.freeze({
|
|
82
|
-
integration: {
|
|
83
|
-
id: config.id,
|
|
84
|
-
name: config.name,
|
|
85
|
-
...config.description !== void 0 ? { description: config.description } : {},
|
|
86
|
-
auth: config.auth,
|
|
87
|
-
...config.proxy ? { proxy: config.proxy } : {},
|
|
88
|
-
...config.needsRawSecret === true ? { needsRawSecret: true } : {},
|
|
89
|
-
...config.connections ? { connections: config.connections } : {}
|
|
90
|
-
},
|
|
91
|
-
credentialSet,
|
|
92
|
-
internalCredentialSets,
|
|
93
|
-
allInternalCredentialSets
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
//#endregion
|
|
98
|
-
//#region src/_official/provider-app.ts
|
|
99
|
-
const shopifyAppCredentialSet = new CredentialSet({
|
|
100
|
-
id: officialCredentialSetId("shopify-app"),
|
|
101
|
-
exposure: "platform-only",
|
|
102
|
-
name: "Shopify App",
|
|
103
|
-
auth: z.object({
|
|
104
|
-
clientId: z.string(),
|
|
105
|
-
clientSecret: z.string(),
|
|
106
|
-
webhookSecret: z.string()
|
|
107
|
-
})
|
|
108
|
-
});
|
|
109
|
-
const shopifyOfficialProviderSeed = {
|
|
110
|
-
provider: "shopify",
|
|
111
|
-
appRef: "shopify-platform",
|
|
112
|
-
displayName: "Shopify Platform",
|
|
113
|
-
credentialSetName: "Keystroke Shopify Platform App",
|
|
114
|
-
envShape: {
|
|
115
|
-
KEYSTROKE_OFFICIAL_SHOPIFY_APP_ID: z.string().optional(),
|
|
116
|
-
KEYSTROKE_OFFICIAL_SHOPIFY_CLIENT_ID: z.string().optional(),
|
|
117
|
-
KEYSTROKE_OFFICIAL_SHOPIFY_CLIENT_SECRET: z.string().optional(),
|
|
118
|
-
KEYSTROKE_OFFICIAL_SHOPIFY_WEBHOOK_SECRET: z.string().optional()
|
|
119
|
-
},
|
|
120
|
-
requiredEnvKeys: [
|
|
121
|
-
"KEYSTROKE_OFFICIAL_SHOPIFY_APP_ID",
|
|
122
|
-
"KEYSTROKE_OFFICIAL_SHOPIFY_CLIENT_ID",
|
|
123
|
-
"KEYSTROKE_OFFICIAL_SHOPIFY_CLIENT_SECRET",
|
|
124
|
-
"KEYSTROKE_OFFICIAL_SHOPIFY_WEBHOOK_SECRET"
|
|
125
|
-
],
|
|
126
|
-
externalAppIdEnvKey: "KEYSTROKE_OFFICIAL_SHOPIFY_APP_ID",
|
|
127
|
-
buildCredentials: (env) => ({
|
|
128
|
-
clientId: env.KEYSTROKE_OFFICIAL_SHOPIFY_CLIENT_ID,
|
|
129
|
-
clientSecret: env.KEYSTROKE_OFFICIAL_SHOPIFY_CLIENT_SECRET,
|
|
130
|
-
webhookSecret: env.KEYSTROKE_OFFICIAL_SHOPIFY_WEBHOOK_SECRET
|
|
131
|
-
})
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
//#endregion
|
|
135
|
-
//#region ../../packages/credential-connection/dist/defaults-YE9AvLIc.mjs
|
|
136
|
-
/**
|
|
137
|
-
* Shared OAuth 2.0 token-response plumbing.
|
|
138
|
-
*
|
|
139
|
-
* `parseOAuthTokenResponse` handles the two common wire formats (JSON and
|
|
140
|
-
* form-urlencoded, the latter used by GitHub unless you explicitly ask for
|
|
141
|
-
* JSON). `normalizeOAuthTokens` pulls the standard fields out of the parsed
|
|
142
|
-
* body in both the flat form and Slack's nested `authed_user.access_token`
|
|
143
|
-
* form. These helpers are exposed so integration authors overriding
|
|
144
|
-
* `exchangeCode` / `refreshToken` do not have to re-implement them.
|
|
145
|
-
*/
|
|
146
|
-
var TokenExchangeError = class extends Error {
|
|
147
|
-
httpStatus;
|
|
148
|
-
constructor(httpStatus) {
|
|
149
|
-
super(`Token exchange failed: HTTP ${httpStatus}`);
|
|
150
|
-
this.name = "TokenExchangeError";
|
|
151
|
-
this.httpStatus = httpStatus;
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
async function parseOAuthTokenResponse(response) {
|
|
155
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
156
|
-
if (contentType.includes("application/json")) return await response.json();
|
|
157
|
-
const text = await response.text();
|
|
158
|
-
if (!text.trim()) return {};
|
|
159
|
-
if (contentType.includes("application/x-www-form-urlencoded") || text.includes("=")) {
|
|
160
|
-
const params = new URLSearchParams(text);
|
|
161
|
-
return Object.fromEntries(params.entries());
|
|
162
|
-
}
|
|
163
|
-
throw new Error(`Unsupported OAuth token response content type: ${contentType || "unknown"}`);
|
|
164
|
-
}
|
|
165
|
-
function normalizeOAuthTokens(tokenData) {
|
|
166
|
-
const authedUser = tokenData.authed_user;
|
|
167
|
-
const rawAccessToken = tokenData.access_token ?? authedUser?.access_token;
|
|
168
|
-
const rawRefreshToken = tokenData.refresh_token;
|
|
169
|
-
const rawExpiresIn = tokenData.expires_in;
|
|
170
|
-
const rawInstanceUrl = tokenData.instance_url;
|
|
171
|
-
return {
|
|
172
|
-
accessToken: typeof rawAccessToken === "string" ? rawAccessToken : void 0,
|
|
173
|
-
refreshToken: typeof rawRefreshToken === "string" ? rawRefreshToken : void 0,
|
|
174
|
-
expiresIn: typeof rawExpiresIn === "number" ? rawExpiresIn : typeof rawExpiresIn === "string" ? Number(rawExpiresIn) || void 0 : void 0,
|
|
175
|
-
instanceUrl: typeof rawInstanceUrl === "string" ? rawInstanceUrl : void 0
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
//#endregion
|
|
180
|
-
//#region src/oauth-connection.ts
|
|
181
|
-
/**
|
|
182
|
-
* Shopify OAuth connection configuration.
|
|
183
|
-
*
|
|
184
|
-
* Shopify's OAuth flow is per-shop: every handshake is scoped to a specific
|
|
185
|
-
* `{shop}.myshopify.com` domain that the user supplies up front. That shop
|
|
186
|
-
* domain shows up in three places that make Shopify different from a plain
|
|
187
|
-
* OAuth 2.0 integration:
|
|
188
|
-
*
|
|
189
|
-
* - `buildAuthUrl` must point at the shop-specific authorize endpoint at
|
|
190
|
-
* `https://{shop}.myshopify.com/admin/oauth/authorize`. The shop domain is
|
|
191
|
-
* threaded in from the initiate-connection request via `initiateInput`.
|
|
192
|
-
* - `exchangeCode` verifies the Shopify HMAC signature on the callback query
|
|
193
|
-
* string before trusting any of it, and POSTs to the same shop-specific
|
|
194
|
-
* token endpoint. HMAC verification uses `timingSafeEqual` to avoid leaking
|
|
195
|
-
* bytes through timing side channels.
|
|
196
|
-
* - `refreshToken` reads the shop domain out of `oauthClient.metadata` (set
|
|
197
|
-
* during the initial exchange) so proactive refreshes still target the
|
|
198
|
-
* right store, and the `vault` mapping promotes the shop domain into
|
|
199
|
-
* `SHOPIFY_STORE_DOMAIN` so the vault has both the access token and the
|
|
200
|
-
* store it belongs to.
|
|
201
|
-
*
|
|
202
|
-
* Shopify responds with JSON, so we reuse `parseOAuthTokenResponse` +
|
|
203
|
-
* `normalizeOAuthTokens` from `@keystrokehq/credential-connection` to avoid reimplementing
|
|
204
|
-
* the token-response plumbing.
|
|
205
|
-
*/
|
|
206
|
-
function normalizeShopifyShopDomain(raw) {
|
|
207
|
-
const trimmed = raw?.trim().toLowerCase();
|
|
208
|
-
if (!trimmed) throw new Error("Shopify requires a store domain.");
|
|
209
|
-
const normalized = trimmed.endsWith(".myshopify.com") ? trimmed : `${trimmed}.myshopify.com`;
|
|
210
|
-
if (!/^[a-z0-9][a-z0-9-]*\.myshopify\.com$/i.test(normalized)) throw new Error("Invalid Shopify store domain.");
|
|
211
|
-
return normalized;
|
|
212
|
-
}
|
|
213
|
-
function verifyShopifyCallbackHmac(queryParams, clientSecret) {
|
|
214
|
-
const receivedHmac = queryParams.hmac;
|
|
215
|
-
if (!receivedHmac) return false;
|
|
216
|
-
const canonical = Object.entries(queryParams).filter(([key]) => key !== "hmac" && key !== "signature").sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("&");
|
|
217
|
-
const expected = createHmac("sha256", clientSecret).update(canonical).digest("hex");
|
|
218
|
-
const received = Buffer.from(receivedHmac, "utf8");
|
|
219
|
-
const expectedBuffer = Buffer.from(expected, "utf8");
|
|
220
|
-
if (received.length !== expectedBuffer.length) return false;
|
|
221
|
-
return timingSafeEqual(received, expectedBuffer);
|
|
222
|
-
}
|
|
223
|
-
function resolveShopifyTokenUrl(shopDomain) {
|
|
224
|
-
return `https://${shopDomain}/admin/oauth/access_token`;
|
|
225
|
-
}
|
|
226
|
-
async function shopifyTokenPost(shopDomain, body, missingTokenError) {
|
|
227
|
-
const response = await fetch(resolveShopifyTokenUrl(shopDomain), {
|
|
228
|
-
method: "POST",
|
|
229
|
-
headers: {
|
|
230
|
-
Accept: "application/json",
|
|
231
|
-
"Content-Type": "application/json"
|
|
232
|
-
},
|
|
233
|
-
body: JSON.stringify(body)
|
|
234
|
-
});
|
|
235
|
-
if (!response.ok) throw new TokenExchangeError(response.status);
|
|
236
|
-
const raw = await parseOAuthTokenResponse(response);
|
|
237
|
-
raw._shopifyShopDomain = shopDomain;
|
|
238
|
-
const { accessToken, refreshToken, expiresIn } = normalizeOAuthTokens(raw);
|
|
239
|
-
if (!accessToken) throw new Error(missingTokenError);
|
|
240
|
-
return {
|
|
241
|
-
accessToken,
|
|
242
|
-
refreshToken,
|
|
243
|
-
expiresIn,
|
|
244
|
-
raw
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
const shopifyOAuthConnection = {
|
|
248
|
-
kind: "oauth",
|
|
249
|
-
tokenType: "refreshable",
|
|
250
|
-
authUrl: "https://{shop}.myshopify.com/admin/oauth/authorize",
|
|
251
|
-
tokenUrl: "https://{shop}.myshopify.com/admin/oauth/access_token",
|
|
252
|
-
revokeUrl: null,
|
|
253
|
-
scopes: [
|
|
254
|
-
"read_products",
|
|
255
|
-
"write_products",
|
|
256
|
-
"read_orders",
|
|
257
|
-
"write_orders",
|
|
258
|
-
"read_customers",
|
|
259
|
-
"write_customers",
|
|
260
|
-
"read_inventory",
|
|
261
|
-
"write_inventory",
|
|
262
|
-
"read_locations",
|
|
263
|
-
"read_fulfillments",
|
|
264
|
-
"write_fulfillments",
|
|
265
|
-
"read_content",
|
|
266
|
-
"write_content",
|
|
267
|
-
"read_webhooks",
|
|
268
|
-
"write_webhooks"
|
|
269
|
-
],
|
|
270
|
-
vault: {
|
|
271
|
-
accessToken: "SHOPIFY_ACCESS_TOKEN",
|
|
272
|
-
raw: { SHOPIFY_STORE_DOMAIN: "_shopifyShopDomain" }
|
|
273
|
-
},
|
|
274
|
-
buildAuthUrl({ oauthClient, redirectUri, state, connection, initiateInput, requestedScopes }) {
|
|
275
|
-
const rawShopDomain = initiateInput?.shopDomain;
|
|
276
|
-
if (typeof rawShopDomain !== "string") throw new Error("Shopify requires a store domain.");
|
|
277
|
-
const scopes = requestedScopes ?? connection.scopes;
|
|
278
|
-
const shopDomain = normalizeShopifyShopDomain(rawShopDomain);
|
|
279
|
-
const url = new URL(`https://${shopDomain}/admin/oauth/authorize`);
|
|
280
|
-
url.searchParams.set("client_id", oauthClient.clientId);
|
|
281
|
-
url.searchParams.set("scope", scopes.join(","));
|
|
282
|
-
url.searchParams.set("redirect_uri", redirectUri);
|
|
283
|
-
url.searchParams.set("state", state);
|
|
284
|
-
return url.toString();
|
|
285
|
-
},
|
|
286
|
-
async exchangeCode({ oauthClient, code, queryParams }) {
|
|
287
|
-
if (!code) throw new Error("Missing authorization code");
|
|
288
|
-
const shopDomain = normalizeShopifyShopDomain(queryParams.shop);
|
|
289
|
-
if (!verifyShopifyCallbackHmac(queryParams, oauthClient.clientSecret)) throw new Error("Invalid Shopify OAuth callback signature");
|
|
290
|
-
return shopifyTokenPost(shopDomain, {
|
|
291
|
-
client_id: oauthClient.clientId,
|
|
292
|
-
client_secret: oauthClient.clientSecret,
|
|
293
|
-
code
|
|
294
|
-
}, "No access token in Shopify response");
|
|
295
|
-
},
|
|
296
|
-
async refreshToken({ oauthClient, refreshToken }) {
|
|
297
|
-
const rawShopDomain = oauthClient.metadata?.shopDomain;
|
|
298
|
-
if (typeof rawShopDomain !== "string") throw new Error("Missing Shopify store domain for token refresh.");
|
|
299
|
-
return shopifyTokenPost(normalizeShopifyShopDomain(rawShopDomain), {
|
|
300
|
-
grant_type: "refresh_token",
|
|
301
|
-
refresh_token: refreshToken,
|
|
302
|
-
client_id: oauthClient.clientId,
|
|
303
|
-
client_secret: oauthClient.clientSecret
|
|
304
|
-
}, "No access token in Shopify refresh response");
|
|
305
|
-
},
|
|
306
|
-
extractInstallationInfo({ tokenResult }) {
|
|
307
|
-
const shopDomain = typeof tokenResult.raw._shopifyShopDomain === "string" ? tokenResult.raw._shopifyShopDomain : void 0;
|
|
308
|
-
return {
|
|
309
|
-
externalInstallationId: shopDomain,
|
|
310
|
-
externalWorkspaceId: shopDomain,
|
|
311
|
-
metadata: shopDomain ? { shopDomain } : void 0
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
//#endregion
|
|
317
|
-
//#region src/integration.ts
|
|
318
|
-
const shopifyStoreDomainSchema = z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*\.myshopify\.com$/i, "Shopify store domain must look like \"example.myshopify.com\".");
|
|
319
|
-
const shopifyAuthSchema = z.object({
|
|
320
|
-
SHOPIFY_STORE_DOMAIN: shopifyStoreDomainSchema,
|
|
321
|
-
SHOPIFY_ACCESS_TOKEN: z.string().min(1)
|
|
322
|
-
});
|
|
323
|
-
const shopifyOfficialIntegration = {
|
|
324
|
-
id: "shopify",
|
|
325
|
-
name: "Shopify",
|
|
326
|
-
description: "Shopify Admin API — stores, products, orders, customers, and webhooks",
|
|
327
|
-
auth: shopifyAuthSchema,
|
|
328
|
-
connections: [{
|
|
329
|
-
id: "oauth",
|
|
330
|
-
...shopifyOAuthConnection
|
|
331
|
-
}]
|
|
332
|
-
};
|
|
333
|
-
const shopifyBundle = defineOfficialIntegration({
|
|
334
|
-
...shopifyOfficialIntegration,
|
|
335
|
-
internal: { providerApp: shopifyAppCredentialSet }
|
|
336
|
-
});
|
|
337
|
-
const shopify = shopifyBundle.credentialSet;
|
|
338
|
-
|
|
339
|
-
//#endregion
|
|
340
|
-
export { shopifyAppCredentialSet as a, shopifyStoreDomainSchema as i, shopifyBundle as n, shopifyOfficialProviderSeed as o, shopifyOfficialIntegration as r, createOfficialOperationFactory as s, shopify as t };
|