@tallyui/connector-woocommerce 3.0.0-next.1 → 3.0.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/README.md CHANGED
@@ -35,7 +35,8 @@ The incremental pull can miss an edit: a `modified_after` that over-excludes (in
35
35
 
36
36
  ## Errors
37
37
 
38
- - `ConnectorUnauthorizedError`, from `@tallyui/core` and re-exported here: the store rejected the token (401 or 403), or `WooMissingTokenError` (a subclass) was thrown for a missing token. `error.status` is `401` (`code: 'unauthorized'`, sign in again) or `403` (`code: 'forbidden'`, signed in but not allowed: the pull retries on the store schedule; don't sign out).
38
+ - `ConnectorUnauthorizedError`, from `@tallyui/core` and re-exported here: the store refused the request (401: the token is missing, expired or revoked; 403: signed in but not allowed), or `WooMissingTokenError` (a subclass) was thrown for a missing token. `error.status` is `401` (`code: 'unauthorized'`, sign in again) or `403` (`code: 'forbidden'`, signed in but not allowed: the pull retries on the store schedule; don't sign out).
39
+ - `WooTokenRefusedError` (`code: 'store_misconfigured'`, `fixedBy: 'store'`, `fix`, and the store's own `storeCode`): the store answered a `jwt_auth_*` 403, the JWT Authentication plugin refusing the till's token. WCPOS 1.10.0–1.10.7 sent it for a valid token (wcpos#1863), and later versions can still send it when the token resolves to another user, so the error names no version. The store owner checks the JWT Authentication plugin's settings, or the till is paired again; the pull retries on the store schedule and the till stays signed in.
39
40
  - `WooDateFilterError` (`code: 'unsupported_store'`): the store returned a product outside the requested date window, so it does not honour `modified_after` (WooCommerce before 5.8, or a proxy that drops the parameter). An app should show its own words for the `code`; the message, `This store needs WooCommerce 5.8 or later to sync products.`, is a plain fallback. The diagnostics are in `productId`, `bound` (the `modified_after` sent) and `received`. Today the pull retries a failed request every 5 s (RxDB's `retryTime`), one mark request each time. Stopping on errors that cannot recover is shared replication work.
40
41
  - `WooTillUpdateRequiredError` (`code: 'till_update_required'`, `fixedBy: 'till'`): the store answered 426, normally WCPOS's protocol gate (`serverCode: 'wcpos_update_required'`), so this till must be updated; the pull pauses until `resume()`.
41
42
  - `WooMissingUuidError`: a product came back without a `uuid`, so the store is not running the WCPOS Free plugin 1.10.0 or later, or the connector is not reaching it through `wcpos/v2`.
package/dist/index.d.ts CHANGED
@@ -69,6 +69,21 @@ declare class WooDateFilterError extends Error {
69
69
  readonly minVersion = "5.8";
70
70
  constructor(productId: number | undefined, bound: string, received: string | undefined);
71
71
  }
72
+ /**
73
+ * A `jwt_auth_*` 403: the store's JWT Authentication plugin refused the till's token. WCPOS 1.10.0–1.10.7 sent it for a valid token
74
+ * (wcpos/woocommerce-pos#1863); 1.10.8 and later can still send it when the token resolves to another user. So the
75
+ * message names no version: a version hint belongs only where a response itself carries the WCPOS version (#360).
76
+ */
77
+ declare class WooTokenRefusedError extends Error {
78
+ readonly storeCode: string;
79
+ name: string;
80
+ readonly code: "store_misconfigured";
81
+ /** The store owner fixes it on the store, so the pull waits the store delay and the till stays signed in (`errorKind`). */
82
+ readonly fixedBy: "store";
83
+ /** The store-side remedy, for `SyncStatus`'s detail. */
84
+ readonly fix = "check the JWT Authentication plugin's settings";
85
+ constructor(storeCode: string);
86
+ }
72
87
  /**
73
88
  * Replication adapter for WooCommerce products.
74
89
  *
@@ -124,4 +139,4 @@ declare function createWooCommerceConnector(): TallyConnector;
124
139
  */
125
140
  declare const woocommerceConnector: TallyConnector;
126
141
 
127
- export { WooDateFilterError, WooMissingTokenError, WooMissingUuidError, WooTillUpdateRequiredError, createWooCommerceConnector, wcposClientPart, wooCatalogueReconcile, wooProductReplication, wooProductSchema, wooProductSync, wooProductTraits, wooReconcileFingerprint, woocommerceConnector };
142
+ export { WooDateFilterError, WooMissingTokenError, WooMissingUuidError, WooTillUpdateRequiredError, WooTokenRefusedError, createWooCommerceConnector, wcposClientPart, wooCatalogueReconcile, wooProductReplication, wooProductSchema, wooProductSync, wooProductTraits, wooReconcileFingerprint, woocommerceConnector };
package/dist/index.js CHANGED
@@ -259,6 +259,8 @@ var MAX_STORE_MESSAGE = 200;
259
259
  async function checkResponse(response) {
260
260
  if (response.ok) return;
261
261
  if (response.status === 401 || response.status === 403) {
262
+ const body = response.status === 403 ? await response.json().catch(() => void 0) : void 0;
263
+ if (typeof body?.code === "string" && body.code.startsWith("jwt_auth_")) throw new WooTokenRefusedError(body.code);
262
264
  throw new ConnectorUnauthorizedError(`WooCommerce API error: ${response.status}`, response.status);
263
265
  }
264
266
  if (response.status === 426) {
@@ -313,6 +315,18 @@ var WooDateFilterError = class extends Error {
313
315
  /** The first WooCommerce version that applies `modified_after` (5.8). */
314
316
  minVersion = "5.8";
315
317
  };
318
+ var WooTokenRefusedError = class extends Error {
319
+ constructor(storeCode) {
320
+ super("The store refused the sign-in token (403). Ask the store owner to check the JWT Authentication plugin's settings, or pair the till again.");
321
+ this.storeCode = storeCode;
322
+ }
323
+ name = "WooTokenRefusedError";
324
+ code = "store_misconfigured";
325
+ /** The store owner fixes it on the store, so the pull waits the store delay and the till stays signed in (`errorKind`). */
326
+ fixedBy = "store";
327
+ /** The store-side remedy, for `SyncStatus`'s detail. */
328
+ fix = "check the JWT Authentication plugin's settings";
329
+ };
316
330
  var wooProductReplication = {
317
331
  pull: {
318
332
  handler: async function pull(lastCheckpoint, batchSize, context, requests = 0) {
@@ -425,6 +439,7 @@ function wooCatalogueReconcile(feed) {
425
439
  return;
426
440
  }
427
441
  if (!failed) console.warn("WooCommerce catalogue reconcile: the fast path answered with something that is not a list; listing page by page.");
442
+ yield { entries: [], cursor: from };
428
443
  }
429
444
  for (let page = from; ; page++) {
430
445
  const params = new URLSearchParams({
@@ -448,8 +463,8 @@ function wooCatalogueReconcile(feed) {
448
463
  if (!ids.length) return [];
449
464
  const params = new URLSearchParams({ include: ids.join(","), per_page: String(PAGE_SIZE), status: "any", _fields: "id,uuid,status" });
450
465
  const rows = await get(`/products?${params}`, context);
451
- const live = new Set(rows.filter((row) => row.status === "publish").map((row) => row.id));
452
- return locals.filter((doc) => Number.isInteger(doc.id) && !live.has(doc.id)).map((doc) => doc.uuid);
466
+ const live = new Map(rows.filter((row) => row.status === "publish").map((row) => [row.id, typeof row.uuid === "string" && row.uuid.length > 0 ? row.uuid : void 0]));
467
+ return locals.filter((doc) => Number.isInteger(doc.id) && (!live.has(doc.id) || typeof live.get(doc.id) === "string" && live.get(doc.id) !== doc.uuid)).map((doc) => doc.uuid);
453
468
  },
454
469
  // An entry with no numeric id, locally or in the listing, is dropped: the store cannot be asked about it, so
455
470
  // the feed would find nothing and tombstone it without proof. (The schema requires only uuid.)
@@ -480,7 +495,7 @@ function createWooReconcileFeed() {
480
495
  }
481
496
 
482
497
  // package.json
483
- var version = "3.0.0-next.1";
498
+ var version = "3.0.0";
484
499
 
485
500
  // src/index.ts
486
501
  import { ConnectorUnauthorizedError as ConnectorUnauthorizedError3 } from "@tallyui/core";
@@ -563,6 +578,7 @@ export {
563
578
  WooMissingTokenError,
564
579
  WooMissingUuidError,
565
580
  WooTillUpdateRequiredError,
581
+ WooTokenRefusedError,
566
582
  createWooCommerceConnector,
567
583
  wcposClientPart,
568
584
  wooCatalogueReconcile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/connector-woocommerce",
3
- "version": "3.0.0-next.1",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "WooCommerce connector for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -26,10 +26,10 @@
26
26
  "directory": "connectors/woocommerce"
27
27
  },
28
28
  "peerDependencies": {
29
- "@tallyui/core": "^3.0.0-next.1"
29
+ "@tallyui/core": "^3.0.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@tallyui/database": "3.0.0-next.1",
32
+ "@tallyui/database": "3.0.0",
33
33
  "rxdb": "17.5.0"
34
34
  },
35
35
  "scripts": {
package/src/index.ts CHANGED
@@ -119,5 +119,5 @@ export { ConnectorUnauthorizedError } from '@tallyui/core';
119
119
  export { wooProductSchema } from './schemas/products';
120
120
  export { wooProductTraits } from './traits/product';
121
121
  export { wooProductSync } from './sync/products';
122
- export { wooProductReplication, WooDateFilterError, WooMissingUuidError, WooTillUpdateRequiredError } from './replication/products';
122
+ export { wooProductReplication, WooDateFilterError, WooTokenRefusedError, WooMissingUuidError, WooTillUpdateRequiredError } from './replication/products';
123
123
  export { wooCatalogueReconcile, wooReconcileFingerprint } from './reconcile/catalogue';
@@ -30,8 +30,9 @@ async function get(path: string, context: SyncContext): Promise<any> {
30
30
  /**
31
31
  * The switch for the bulk-id fast path: true only when wcpos/v2/status lists `products_id_fast_path` in
32
32
  * `capabilities` (a missing field is false). Never a version number, and never a failed attempt.
33
- * A failed read (network, non-OK, non-JSON) is "no capability known", so the paged listing runs; only a
34
- * till-class error (401, 403, 426 through checkResponse) stops the pass, since the listing would fail the same way.
33
+ * A failed status read is "no capability known"; only a 401 or 403 would make the paged listing fail the same way.
34
+ * Only a till-class error (401 `unauthorized`, 426 `till_update_required`) or an abort stops the pass.
35
+ * A 403 `forbidden` is store-class: the whole pass is logged `skipped` and runs again at the next check (hourly by default).
35
36
  */
36
37
  export async function wooHasIdFastPath(context: SyncContext): Promise<boolean> {
37
38
  let status: any;
@@ -69,7 +70,8 @@ export function wooCatalogueReconcile(feed: Pick<ReconcileFeed<any>, 'enqueue'>)
69
70
  yield { entries: [], cursor: from };
70
71
  if (fast) {
71
72
  // The whole catalogue in one request, so `from` is moot. A store that refuses it (a store scope answers 400)
72
- // is listed page by page in this pass; only a till-class error or an abort stops it.
73
+ // falls back to the paged listing, which fails the same way. A 403 `forbidden` is skipped until the next hourly
74
+ // check; only a till-class error or an abort stops the pass.
73
75
  let rows: any;
74
76
  let failed = false;
75
77
  try {
@@ -84,6 +86,8 @@ export function wooCatalogueReconcile(feed: Pick<ReconcileFeed<any>, 'enqueue'>)
84
86
  return;
85
87
  }
86
88
  if (!failed) console.warn('WooCommerce catalogue reconcile: the fast path answered with something that is not a list; listing page by page.');
89
+ // An empty page, so the fallback's first request takes its own budget slot, as the status read does.
90
+ yield { entries: [], cursor: from };
87
91
  }
88
92
  // status=publish: a product that is not published is absent, so it becomes a deletion candidate and
89
93
  // confirmGone decides; drafts cost no daily refetch.
@@ -104,9 +108,11 @@ export function wooCatalogueReconcile(feed: Pick<ReconcileFeed<any>, 'enqueue'>)
104
108
  // An empty include= would list the whole store; a local without a numeric id has no proof and is kept.
105
109
  if (!ids.length) return [];
106
110
  const params = new URLSearchParams({ include: ids.join(','), per_page: String(PAGE_SIZE), status: 'any', _fields: 'id,uuid,status' });
107
- const rows: Array<{ id: number; status: string }> = await get(`/products?${params}`, context);
108
- const live = new Set(rows.filter((row) => row.status === 'publish').map((row) => row.id));
109
- return locals.filter((doc) => Number.isInteger(doc.id) && !live.has(doc.id)).map((doc) => doc.uuid);
111
+ const rows: Array<{ id: number; uuid: string; status: string }> = await get(`/products?${params}`, context);
112
+ const live = new Map(rows.filter((row) => row.status === 'publish').map((row) => [row.id, typeof row.uuid === 'string' && row.uuid.length > 0 ? row.uuid : undefined]));
113
+ // A live id backs only the store's uuid; other local copies are gone (#369).
114
+ // A row without a uuid can't prove the local copy stale, so a live id keeps it (#375 review).
115
+ return locals.filter((doc) => Number.isInteger(doc.id) && (!live.has(doc.id) || (typeof live.get(doc.id) === 'string' && live.get(doc.id) !== doc.uuid))).map((doc) => doc.uuid);
110
116
  },
111
117
  // An entry with no numeric id, locally or in the listing, is dropped: the store cannot be asked about it, so
112
118
  // the feed would find nothing and tombstone it without proof. (The schema requires only uuid.)
@@ -21,6 +21,9 @@ const MAX_STORE_MESSAGE = 200;
21
21
  export async function checkResponse(response: Response) {
22
22
  if (response.ok) return;
23
23
  if (response.status === 401 || response.status === 403) {
24
+ // A jwt_auth_* 403 is the store's JWT layer refusing the token, on any WCPOS version (#360).
25
+ const body = response.status === 403 ? await response.json().catch(() => undefined) : undefined;
26
+ if (typeof body?.code === 'string' && body.code.startsWith('jwt_auth_')) throw new WooTokenRefusedError(body.code);
24
27
  throw new ConnectorUnauthorizedError(`WooCommerce API error: ${response.status}`, response.status as 401 | 403);
25
28
  }
26
29
  if (response.status === 426) {
@@ -87,6 +90,24 @@ export class WooDateFilterError extends Error {
87
90
  }
88
91
  }
89
92
 
93
+ /**
94
+ * A `jwt_auth_*` 403: the store's JWT Authentication plugin refused the till's token. WCPOS 1.10.0–1.10.7 sent it for a valid token
95
+ * (wcpos/woocommerce-pos#1863); 1.10.8 and later can still send it when the token resolves to another user. So the
96
+ * message names no version: a version hint belongs only where a response itself carries the WCPOS version (#360).
97
+ */
98
+ export class WooTokenRefusedError extends Error {
99
+ name = 'WooTokenRefusedError';
100
+ readonly code = 'store_misconfigured' as const;
101
+ /** The store owner fixes it on the store, so the pull waits the store delay and the till stays signed in (`errorKind`). */
102
+ readonly fixedBy = 'store' as const;
103
+ /** The store-side remedy, for `SyncStatus`'s detail. */
104
+ readonly fix = "check the JWT Authentication plugin's settings";
105
+
106
+ constructor(readonly storeCode: string) {
107
+ super("The store refused the sign-in token (403). Ask the store owner to check the JWT Authentication plugin's settings, or pair the till again.");
108
+ }
109
+ }
110
+
90
111
  /**
91
112
  * Replication adapter for WooCommerce products.
92
113
  *