@porulle/adapter-shopify 0.67.0 → 0.68.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/adapter-shopify",
3
- "version": "0.67.0",
3
+ "version": "0.68.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,12 +16,14 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "zod": "^4.1.11",
19
- "@porulle/core": "0.67.0"
19
+ "@porulle/core": "0.68.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^24.5.2",
23
+ "esbuild": "0.25.12",
23
24
  "eslint": "^9.39.1",
24
25
  "graphql": "^16.11.0",
26
+ "miniflare": "4.20260722.1",
25
27
  "typescript": "5.9.2",
26
28
  "vitest": "^3.2.4",
27
29
  "@porulle/eslint-config": "0.1.0",
package/src/graphql.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Err, Ok } from "@porulle/core";
1
+ import { CHANNEL_CREDENTIALS_REJECTED, Err, Ok } from "@porulle/core";
2
2
  import type { ChannelConnectorError, Result } from "@porulle/core";
3
3
  import { z } from "zod";
4
4
 
@@ -73,6 +73,10 @@ export async function shopifyGraphql<T>(
73
73
  schema: z.ZodType<T>,
74
74
  ): Promise<Result<T, ChannelConnectorError>> {
75
75
  const sleep = target.sleep ?? defaultSleep;
76
+ // Called unbound, never as `target.fetchImpl(...)`: workerd's global `fetch` refuses any `this`
77
+ // but the global scope ("Illegal invocation"), and Node's does not, so a method call passes on
78
+ // Node and fails every Admin API call on a Worker. test/workerd-fetch.e2e.test.ts holds it.
79
+ const { fetchImpl } = target;
76
80
  const url = `${target.origin}/admin/api/${SHOPIFY_API_VERSION}/graphql.json`;
77
81
  let waited = 0;
78
82
  for (let attempt = 0; ; attempt += 1) {
@@ -80,7 +84,7 @@ export async function shopifyGraphql<T>(
80
84
  try {
81
85
  // `manual`, never `error`: workerd implements only `follow` and `manual` and throws on `error`
82
86
  // from the Request constructor. A 3xx is then simply not ok, which refuses the redirect.
83
- response = await target.fetchImpl(url, {
87
+ response = await fetchImpl(url, {
84
88
  method: "POST",
85
89
  redirect: "manual",
86
90
  headers: { accept: "application/json", "content-type": "application/json", "x-shopify-access-token": target.accessToken },
@@ -90,7 +94,7 @@ export async function shopifyGraphql<T>(
90
94
  return Err({ code: "SHOPIFY_API_FAILED", message: error instanceof Error ? error.message : "Shopify API request failed.", retriable: true });
91
95
  }
92
96
  if (response.status === 401 || response.status === 403) {
93
- return Err({ code: "SHOPIFY_UNAUTHORIZED", message: `Shopify refused the access token (${response.status}); the store must be reconnected.`, retriable: false });
97
+ return Err({ code: CHANNEL_CREDENTIALS_REJECTED, message: `Shopify refused the access token (${response.status}).`, retriable: false });
94
98
  }
95
99
  if (response.status !== 429 && !response.ok) {
96
100
  return Err({ code: "SHOPIFY_API_FAILED", message: `Shopify API request failed (${response.status}).`, retriable: response.status >= 500 });
package/src/index.ts CHANGED
@@ -152,10 +152,10 @@ export function shopifyConnector(options: ShopifyConnectorOptions): ChannelConne
152
152
  if (!credentials.ok) return credentials;
153
153
  return Ok({ credentials: { ...credentials.value }, storeDomain: shop });
154
154
  },
155
- async liveCredentials(store) {
155
+ async liveCredentials(store, refresh) {
156
156
  const credentials = parseShopifyCredentials(store.credentials);
157
157
  if (!credentials) return Err(credentialsRequired);
158
- const refreshed = await refreshIfExpiring({ fetchImpl, origin: origin(store.storeDomain), clientId: options.clientId, clientSecret: options.clientSecret, credentials, now: Date.now() });
158
+ const refreshed = await refreshIfExpiring({ fetchImpl, origin: origin(store.storeDomain), clientId: options.clientId, clientSecret: options.clientSecret, credentials, now: Date.now(), ...(refresh?.force === true ? { force: true } : {}) });
159
159
  if (!refreshed.ok) return refreshed;
160
160
  return Ok(refreshed.value === null ? null : { ...refreshed.value });
161
161
  },
package/src/oauth.ts CHANGED
@@ -181,11 +181,15 @@ export async function refreshIfExpiring(params: {
181
181
  clientSecret: string;
182
182
  credentials: ShopifyCredentials;
183
183
  now: number;
184
+ /** Refresh even though the stated expiry is far off: Shopify just rejected the token. */
185
+ force?: boolean;
184
186
  }): Promise<Result<ShopifyCredentials | null, ChannelConnectorError>> {
185
187
  const { credentials } = params;
186
- if (credentials.accessTokenExpiresAt === undefined) return Ok(null);
187
- const expiresAt = Date.parse(credentials.accessTokenExpiresAt);
188
- if (Number.isFinite(expiresAt) && expiresAt - params.now > ACCESS_TOKEN_REFRESH_MARGIN_MS) return Ok(null);
188
+ if (params.force !== true) {
189
+ if (credentials.accessTokenExpiresAt === undefined) return Ok(null);
190
+ const expiresAt = Date.parse(credentials.accessTokenExpiresAt);
191
+ if (Number.isFinite(expiresAt) && expiresAt - params.now > ACCESS_TOKEN_REFRESH_MARGIN_MS) return Ok(null);
192
+ }
189
193
  if (credentials.refreshToken === undefined) return oauthError("SHOPIFY_REAUTHORIZATION_REQUIRED", "The store's access token expired and no refresh token is held; the store must be reconnected.");
190
194
  const token = await postTokenEndpoint(params.fetchImpl, params.origin, {
191
195
  client_id: params.clientId,