@zauru-sdk/common 6.12.0 → 6.14.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/CHANGELOG.md +19 -0
- package/dist/common.d.ts +6 -1
- package/dist/esm/common.js +25 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/sessionReauth.js +65 -0
- package/dist/index.d.ts +1 -0
- package/dist/sessionReauth.d.ts +21 -0
- package/package.json +4 -4
- package/src/common.ts +31 -0
- package/src/index.ts +1 -0
- package/src/purchaseOrderFinancials.test.ts +64 -0
- package/src/sessionReauth.test.ts +50 -0
- package/src/sessionReauth.ts +95 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [6.14.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.13.0...@zauru-sdk/common@6.14.0) (2026-10-30)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* preservar estado local al reautenticar webapps ([e1f76e6](https://github.com/intuitiva/zauru-typescript-sdk/commit/e1f76e6fe06a76c9dc183b0d1af91439f4cadea7))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# [6.13.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.12.0...@zauru-sdk/common@6.13.0) (2026-10-29)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* armar cost de línea como qty × unit_cost ([707d6b25](https://github.com/intuitiva/zauru-typescript-sdk/commit/707d6b25))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# [6.12.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/common@6.10.1...@zauru-sdk/common@6.12.0) (2026-10-26)
|
|
7
26
|
|
|
8
27
|
|
package/dist/common.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "moment-timezone";
|
|
2
|
-
import type { PayeeGraphQL, AxiosUtilsResponse, BasketSchema, SelectFieldOption, JsonMemoType, CalculatePurchaseOrderFinancialsInput, CalculatePurchaseOrderFinancialsResult, RejectionPercentageLayers } from "@zauru-sdk/types";
|
|
2
|
+
import type { PayeeGraphQL, AxiosUtilsResponse, BasketSchema, SelectFieldOption, JsonMemoType, CalculatePurchaseOrderFinancialsInput, CalculatePurchaseOrderFinancialsResult, PurchaseOrderDetailCostAttribute, PurchaseOrderFinancialDetail, RejectionPercentageLayers } from "@zauru-sdk/types";
|
|
3
3
|
export declare const DESTINOS_MUESTRA_OPTIONS: SelectFieldOption[];
|
|
4
4
|
export declare const parseJsonMemo: (memo?: string | object) => JsonMemoType;
|
|
5
5
|
export declare const stringifyJsonMemo: (memo: JsonMemoType) => string;
|
|
@@ -38,6 +38,11 @@ export declare const resolveRejectionPercentageBase: (memo?: string | object) =>
|
|
|
38
38
|
* `rejectionPercentage` in the result is the effective % that was charged.
|
|
39
39
|
*/
|
|
40
40
|
export declare const calculatePurchaseOrderFinancials: (input: CalculatePurchaseOrderFinancialsInput) => CalculatePurchaseOrderFinancialsResult;
|
|
41
|
+
/**
|
|
42
|
+
* Nested attributes for Zauru: `cost` must be sent with `unit_cost`.
|
|
43
|
+
* Baculo does not recalculate `purchase_order_details.cost` on save.
|
|
44
|
+
*/
|
|
45
|
+
export declare const buildPurchaseOrderDetailsCostAttributes: (details: PurchaseOrderFinancialDetail[] | null | undefined, unitCostByDetailId?: Map<number, number>) => Record<string, PurchaseOrderDetailCostAttribute>;
|
|
41
46
|
/**
|
|
42
47
|
* Obtener el objeto de canastas en base al memo
|
|
43
48
|
* @param memo
|
package/dist/esm/common.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.parsedObject = exports.sortByProperty = exports.labFormPatter = exports.labServicePattern = exports.getRandomNum = exports.CURRENCY_PREFIX = exports.truncateDecimals = exports.ZAURU_REGEX = exports.priceToText = exports.arrayToObject = exports.isNumeric = exports.toFixedIfNeeded = exports.formatTimeToTimePicker = exports.formatDateToDatePicker = exports.getFormattedDate = exports.parsedBaculoFormValue = exports.getPayeeInfoOptions = exports.getPayeeInfoIdOptions = exports.getPayeeFormated = exports.getDateAfterDays = exports.getTimePickerCurrentTime = exports.obtenerFechaActualConZonaHoraria = exports.getDatePickerCurrentDate = exports.stringDateToParsedUTCDate = exports.localDateToUSDate = exports.getStringFullDate = exports.getTodayMinutesDifference = exports.getTodayDaysDifference = exports.getStringDate = exports.getZauruDateByText = exports.getNewDateByFormat = exports.getFechaJuliana = exports.getBasketsSchema = exports.calculatePurchaseOrderFinancials = exports.resolveRejectionPercentageBase = exports.hasSuccessiveRejectionPercentage = exports.getRejectionPercentageLayers = exports.resolveRejectionPercentageLayers = exports.setRejectionPercentage = exports.getRejectionPercentage = exports.mergeJsonMemo = exports.stringifyJsonMemo = exports.parseJsonMemo = exports.DESTINOS_MUESTRA_OPTIONS = void 0;
|
|
6
|
+
exports.parsedObject = exports.sortByProperty = exports.labFormPatter = exports.labServicePattern = exports.getRandomNum = exports.CURRENCY_PREFIX = exports.truncateDecimals = exports.ZAURU_REGEX = exports.priceToText = exports.arrayToObject = exports.isNumeric = exports.toFixedIfNeeded = exports.formatTimeToTimePicker = exports.formatDateToDatePicker = exports.getFormattedDate = exports.parsedBaculoFormValue = exports.getPayeeInfoOptions = exports.getPayeeInfoIdOptions = exports.getPayeeFormated = exports.getDateAfterDays = exports.getTimePickerCurrentTime = exports.obtenerFechaActualConZonaHoraria = exports.getDatePickerCurrentDate = exports.stringDateToParsedUTCDate = exports.localDateToUSDate = exports.getStringFullDate = exports.getTodayMinutesDifference = exports.getTodayDaysDifference = exports.getStringDate = exports.getZauruDateByText = exports.getNewDateByFormat = exports.getFechaJuliana = exports.getBasketsSchema = exports.buildPurchaseOrderDetailsCostAttributes = exports.calculatePurchaseOrderFinancials = exports.resolveRejectionPercentageBase = exports.hasSuccessiveRejectionPercentage = exports.getRejectionPercentageLayers = exports.resolveRejectionPercentageLayers = exports.setRejectionPercentage = exports.getRejectionPercentage = exports.mergeJsonMemo = exports.stringifyJsonMemo = exports.parseJsonMemo = exports.DESTINOS_MUESTRA_OPTIONS = void 0;
|
|
7
7
|
exports.generateClientUUID = generateClientUUID;
|
|
8
8
|
exports.extractValueBetweenTags = extractValueBetweenTags;
|
|
9
9
|
exports.isJsonArray = isJsonArray;
|
|
@@ -174,6 +174,30 @@ const calculatePurchaseOrderFinancials = (input) => {
|
|
|
174
174
|
};
|
|
175
175
|
};
|
|
176
176
|
exports.calculatePurchaseOrderFinancials = calculatePurchaseOrderFinancials;
|
|
177
|
+
/**
|
|
178
|
+
* Nested attributes for Zauru: `cost` must be sent with `unit_cost`.
|
|
179
|
+
* Baculo does not recalculate `purchase_order_details.cost` on save.
|
|
180
|
+
*/
|
|
181
|
+
const buildPurchaseOrderDetailsCostAttributes = (details, unitCostByDetailId) => {
|
|
182
|
+
const attributes = {};
|
|
183
|
+
let index = 0;
|
|
184
|
+
for (const detail of details ?? []) {
|
|
185
|
+
const id = toFiniteNumber(detail.id);
|
|
186
|
+
const itemId = toFiniteNumber(detail.item_id);
|
|
187
|
+
if (id == null || itemId == null)
|
|
188
|
+
continue;
|
|
189
|
+
const unitCost = unitCostByDetailId?.get(id) ?? toFiniteNumber(detail.unit_cost) ?? 0;
|
|
190
|
+
attributes[String(index)] = {
|
|
191
|
+
id,
|
|
192
|
+
item_id: itemId,
|
|
193
|
+
unit_cost: unitCost,
|
|
194
|
+
cost: roundMoney(lineQuantity(detail) * unitCost),
|
|
195
|
+
};
|
|
196
|
+
index += 1;
|
|
197
|
+
}
|
|
198
|
+
return attributes;
|
|
199
|
+
};
|
|
200
|
+
exports.buildPurchaseOrderDetailsCostAttributes = buildPurchaseOrderDetailsCostAttributes;
|
|
177
201
|
/**
|
|
178
202
|
* Obtener el objeto de canastas en base al memo
|
|
179
203
|
* @param memo
|
package/dist/esm/index.js
CHANGED
|
@@ -21,3 +21,4 @@ __exportStar(require("./oauthErrors.js"), exports);
|
|
|
21
21
|
__exportStar(require("./providerRestrictions.js"), exports);
|
|
22
22
|
__exportStar(require("./providerReceptionMatrix.js"), exports);
|
|
23
23
|
__exportStar(require("./rejectionPercentageAdjustment.js"), exports);
|
|
24
|
+
__exportStar(require("./sessionReauth.js"), exports);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isZauruSessionReauthPayload = exports.createZauruSessionReauthPayload = exports.hasZauruOauthReauthCookie = exports.isZauruSessionExpiredError = exports.ZAURU_OAUTH_REAUTH_COOKIE = exports.ZAURU_SESSION_REAUTH_URL = void 0;
|
|
4
|
+
exports.ZAURU_SESSION_REAUTH_URL = "/logout?reauth=1";
|
|
5
|
+
exports.ZAURU_OAUTH_REAUTH_COOKIE = "zauru_oauth_reauth";
|
|
6
|
+
const errorText = (value) => {
|
|
7
|
+
if (value instanceof Error)
|
|
8
|
+
return value.message;
|
|
9
|
+
if (typeof value === "string")
|
|
10
|
+
return value;
|
|
11
|
+
if (value == null)
|
|
12
|
+
return "";
|
|
13
|
+
try {
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const hasUnauthorizedStatus = (value) => {
|
|
21
|
+
if (!value || typeof value !== "object" || !("status" in value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return Number(value.status) === 401;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Detects the HTTP 401 response Zauru emits when the API token stored in the
|
|
28
|
+
* Remix session expires. It accepts direct Response-like values and nested
|
|
29
|
+
* messages produced by handlePossibleAxiosErrors.
|
|
30
|
+
*/
|
|
31
|
+
const isZauruSessionExpiredError = (...values) => {
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
if (hasUnauthorizedStatus(value))
|
|
34
|
+
return true;
|
|
35
|
+
const text = errorText(value);
|
|
36
|
+
const isUnauthorized = /HTTP\s*401/i.test(text) ||
|
|
37
|
+
/status(?:\s+code)?[\s:=]+401/i.test(text) ||
|
|
38
|
+
/401\s+Unauthorized/i.test(text);
|
|
39
|
+
const isFromZauru = /zauru/i.test(text) ||
|
|
40
|
+
/\/apps\/webapp_tables\//i.test(text) ||
|
|
41
|
+
/your session expired/i.test(text);
|
|
42
|
+
if (isUnauthorized && isFromZauru)
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
};
|
|
47
|
+
exports.isZauruSessionExpiredError = isZauruSessionExpiredError;
|
|
48
|
+
const hasZauruOauthReauthCookie = (cookieHeader) => (cookieHeader ?? "")
|
|
49
|
+
.split(";")
|
|
50
|
+
.some((part) => part.trim() === `${exports.ZAURU_OAUTH_REAUTH_COOKIE}=1`);
|
|
51
|
+
exports.hasZauruOauthReauthCookie = hasZauruOauthReauthCookie;
|
|
52
|
+
const createZauruSessionReauthPayload = (overrides = {}) => ({
|
|
53
|
+
error: true,
|
|
54
|
+
reauthRequired: true,
|
|
55
|
+
reauthUrl: overrides.reauthUrl ?? exports.ZAURU_SESSION_REAUTH_URL,
|
|
56
|
+
title: overrides.title ?? "La sesión venció",
|
|
57
|
+
type: overrides.type ?? "info",
|
|
58
|
+
description: overrides.description ??
|
|
59
|
+
"Inicie sesión nuevamente para continuar con la operación.",
|
|
60
|
+
});
|
|
61
|
+
exports.createZauruSessionReauthPayload = createZauruSessionReauthPayload;
|
|
62
|
+
const isZauruSessionReauthPayload = (value) => Boolean(value &&
|
|
63
|
+
typeof value === "object" &&
|
|
64
|
+
value.reauthRequired === true);
|
|
65
|
+
exports.isZauruSessionReauthPayload = isZauruSessionReauthPayload;
|
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const ZAURU_SESSION_REAUTH_URL = "/logout?reauth=1";
|
|
2
|
+
export declare const ZAURU_OAUTH_REAUTH_COOKIE = "zauru_oauth_reauth";
|
|
3
|
+
export type ZauruSessionReauthPayload = {
|
|
4
|
+
error: true;
|
|
5
|
+
reauthRequired: true;
|
|
6
|
+
reauthUrl: string;
|
|
7
|
+
title: string;
|
|
8
|
+
type: "info";
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
type ZauruSessionReauthPayloadOverrides = Partial<Pick<ZauruSessionReauthPayload, "reauthUrl" | "title" | "type" | "description">>;
|
|
12
|
+
/**
|
|
13
|
+
* Detects the HTTP 401 response Zauru emits when the API token stored in the
|
|
14
|
+
* Remix session expires. It accepts direct Response-like values and nested
|
|
15
|
+
* messages produced by handlePossibleAxiosErrors.
|
|
16
|
+
*/
|
|
17
|
+
export declare const isZauruSessionExpiredError: (...values: unknown[]) => boolean;
|
|
18
|
+
export declare const hasZauruOauthReauthCookie: (cookieHeader?: string | null) => boolean;
|
|
19
|
+
export declare const createZauruSessionReauthPayload: (overrides?: ZauruSessionReauthPayloadOverrides) => ZauruSessionReauthPayload;
|
|
20
|
+
export declare const isZauruSessionReauthPayload: (value: unknown) => value is ZauruSessionReauthPayload;
|
|
21
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zauru-sdk/common",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.14.0",
|
|
4
4
|
"description": "Funciones de uso común en el desarrollo de webapps en Zauru",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "npm run build:esm",
|
|
14
14
|
"build:esm": "tsc -p tsconfig.esm.json",
|
|
15
|
-
"test": "tsx --test src/httpClientPolicy.test.ts src/providerRestrictions.test.ts src/providerReceptionMatrix.test.ts"
|
|
15
|
+
"test": "tsx --test src/httpClientPolicy.test.ts src/providerRestrictions.test.ts src/providerReceptionMatrix.test.ts src/purchaseOrderFinancials.test.ts src/sessionReauth.test.ts"
|
|
16
16
|
},
|
|
17
17
|
"author": "Oscar Cuéllar",
|
|
18
18
|
"license": "MIT",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@remix-run/node": "^2.8.1",
|
|
25
|
-
"@zauru-sdk/types": "^6.
|
|
25
|
+
"@zauru-sdk/types": "^6.29.0",
|
|
26
26
|
"moment": "^2.30.1",
|
|
27
27
|
"moment-timezone": "^0.5.45"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "8e5cb991b0da7d1a5d70cd6275d7bfdc52ee1102"
|
|
30
30
|
}
|
package/src/common.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
JsonMemoType,
|
|
10
10
|
CalculatePurchaseOrderFinancialsInput,
|
|
11
11
|
CalculatePurchaseOrderFinancialsResult,
|
|
12
|
+
PurchaseOrderDetailCostAttribute,
|
|
12
13
|
PurchaseOrderFinancialDetail,
|
|
13
14
|
RejectionPercentageLayers,
|
|
14
15
|
} from "@zauru-sdk/types";
|
|
@@ -232,6 +233,36 @@ export const calculatePurchaseOrderFinancials = (
|
|
|
232
233
|
};
|
|
233
234
|
};
|
|
234
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Nested attributes for Zauru: `cost` must be sent with `unit_cost`.
|
|
238
|
+
* Baculo does not recalculate `purchase_order_details.cost` on save.
|
|
239
|
+
*/
|
|
240
|
+
export const buildPurchaseOrderDetailsCostAttributes = (
|
|
241
|
+
details: PurchaseOrderFinancialDetail[] | null | undefined,
|
|
242
|
+
unitCostByDetailId?: Map<number, number>,
|
|
243
|
+
): Record<string, PurchaseOrderDetailCostAttribute> => {
|
|
244
|
+
const attributes: Record<string, PurchaseOrderDetailCostAttribute> = {};
|
|
245
|
+
let index = 0;
|
|
246
|
+
|
|
247
|
+
for (const detail of details ?? []) {
|
|
248
|
+
const id = toFiniteNumber(detail.id);
|
|
249
|
+
const itemId = toFiniteNumber(detail.item_id);
|
|
250
|
+
if (id == null || itemId == null) continue;
|
|
251
|
+
|
|
252
|
+
const unitCost =
|
|
253
|
+
unitCostByDetailId?.get(id) ?? toFiniteNumber(detail.unit_cost) ?? 0;
|
|
254
|
+
attributes[String(index)] = {
|
|
255
|
+
id,
|
|
256
|
+
item_id: itemId,
|
|
257
|
+
unit_cost: unitCost,
|
|
258
|
+
cost: roundMoney(lineQuantity(detail) * unitCost),
|
|
259
|
+
};
|
|
260
|
+
index += 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return attributes;
|
|
264
|
+
};
|
|
265
|
+
|
|
235
266
|
/**
|
|
236
267
|
* Obtener el objeto de canastas en base al memo
|
|
237
268
|
* @param memo
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildPurchaseOrderDetailsCostAttributes,
|
|
5
|
+
calculatePurchaseOrderFinancials,
|
|
6
|
+
} from "./common.js";
|
|
7
|
+
|
|
8
|
+
describe("buildPurchaseOrderDetailsCostAttributes", () => {
|
|
9
|
+
it("sends cost as qty × unit_cost so Baculo can refresh subtotal", () => {
|
|
10
|
+
const attributes = buildPurchaseOrderDetailsCostAttributes(
|
|
11
|
+
[
|
|
12
|
+
{
|
|
13
|
+
id: 2827653,
|
|
14
|
+
item_id: 490543,
|
|
15
|
+
booked_quantity: 14,
|
|
16
|
+
delivered_quantity: 14,
|
|
17
|
+
unit_cost: 8.2,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
new Map([[2827653, 7.7]]),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(attributes, {
|
|
24
|
+
"0": {
|
|
25
|
+
id: 2827653,
|
|
26
|
+
item_id: 490543,
|
|
27
|
+
unit_cost: 7.7,
|
|
28
|
+
cost: 107.8,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps current unit_cost when there is no override", () => {
|
|
34
|
+
const attributes = buildPurchaseOrderDetailsCostAttributes([
|
|
35
|
+
{
|
|
36
|
+
id: 1,
|
|
37
|
+
item_id: 2,
|
|
38
|
+
booked_quantity: 10,
|
|
39
|
+
unit_cost: 4.5,
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
assert.equal(attributes["0"].unit_cost, 4.5);
|
|
44
|
+
assert.equal(attributes["0"].cost, 45);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("calculatePurchaseOrderFinancials", () => {
|
|
49
|
+
it("uses the new unit cost as the rejection discount base", () => {
|
|
50
|
+
const financials = calculatePurchaseOrderFinancials({
|
|
51
|
+
details: [
|
|
52
|
+
{
|
|
53
|
+
booked_quantity: 14,
|
|
54
|
+
delivered_quantity: 14,
|
|
55
|
+
unit_cost: 7.7,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
rejectionPercentage: 7,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
assert.equal(financials.subtotal, 107.8);
|
|
62
|
+
assert.equal(financials.discount, 7.55);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
ZAURU_SESSION_REAUTH_URL,
|
|
5
|
+
createZauruSessionReauthPayload,
|
|
6
|
+
hasZauruOauthReauthCookie,
|
|
7
|
+
isZauruSessionExpiredError,
|
|
8
|
+
isZauruSessionReauthPayload,
|
|
9
|
+
} from "./sessionReauth.js";
|
|
10
|
+
|
|
11
|
+
describe("isZauruSessionExpiredError", () => {
|
|
12
|
+
it("When a Zauru webapp table returns HTTP 401, it should detect an expired session", () => {
|
|
13
|
+
const message =
|
|
14
|
+
"Error: HTTP 401 Unauthorized - URI: https://app.zauru.com/apps/webapp_tables/58/webapp_rows.json - <title>Your session expired</title>";
|
|
15
|
+
|
|
16
|
+
assert.equal(isZauruSessionExpiredError(message), true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("When a Response-like value has status 401, it should detect an expired session", () => {
|
|
20
|
+
assert.equal(isZauruSessionExpiredError({ status: 401 }), true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("When an unrelated Zauru error occurs, it should not request reauthentication", () => {
|
|
24
|
+
assert.equal(
|
|
25
|
+
isZauruSessionExpiredError(
|
|
26
|
+
"HTTP 500 Internal Server Error - URI: https://app.zauru.com/apps/webapp_tables/58/webapp_rows.json",
|
|
27
|
+
),
|
|
28
|
+
false,
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("Zauru session reauthentication", () => {
|
|
34
|
+
it("When the reauth marker cookie is present, it should recognize the refresh flow", () => {
|
|
35
|
+
assert.equal(
|
|
36
|
+
hasZauruOauthReauthCookie("foo=bar; zauru_oauth_reauth=1; theme=dark"),
|
|
37
|
+
true,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("When building a payload, it should keep the standard contract and custom copy", () => {
|
|
42
|
+
const payload = createZauruSessionReauthPayload({
|
|
43
|
+
description: "El registro local se conservará.",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
assert.equal(payload.reauthUrl, ZAURU_SESSION_REAUTH_URL);
|
|
47
|
+
assert.equal(payload.description, "El registro local se conservará.");
|
|
48
|
+
assert.equal(isZauruSessionReauthPayload(payload), true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const ZAURU_SESSION_REAUTH_URL = "/logout?reauth=1";
|
|
2
|
+
export const ZAURU_OAUTH_REAUTH_COOKIE = "zauru_oauth_reauth";
|
|
3
|
+
|
|
4
|
+
export type ZauruSessionReauthPayload = {
|
|
5
|
+
error: true;
|
|
6
|
+
reauthRequired: true;
|
|
7
|
+
reauthUrl: string;
|
|
8
|
+
title: string;
|
|
9
|
+
type: "info";
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ZauruSessionReauthPayloadOverrides = Partial<
|
|
14
|
+
Pick<
|
|
15
|
+
ZauruSessionReauthPayload,
|
|
16
|
+
"reauthUrl" | "title" | "type" | "description"
|
|
17
|
+
>
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
const errorText = (value: unknown): string => {
|
|
21
|
+
if (value instanceof Error) return value.message;
|
|
22
|
+
if (typeof value === "string") return value;
|
|
23
|
+
if (value == null) return "";
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
} catch {
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const hasUnauthorizedStatus = (value: unknown): boolean => {
|
|
33
|
+
if (!value || typeof value !== "object" || !("status" in value)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Number((value as { status?: unknown }).status) === 401;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detects the HTTP 401 response Zauru emits when the API token stored in the
|
|
42
|
+
* Remix session expires. It accepts direct Response-like values and nested
|
|
43
|
+
* messages produced by handlePossibleAxiosErrors.
|
|
44
|
+
*/
|
|
45
|
+
export const isZauruSessionExpiredError = (...values: unknown[]): boolean => {
|
|
46
|
+
for (const value of values) {
|
|
47
|
+
if (hasUnauthorizedStatus(value)) return true;
|
|
48
|
+
|
|
49
|
+
const text = errorText(value);
|
|
50
|
+
const isUnauthorized =
|
|
51
|
+
/HTTP\s*401/i.test(text) ||
|
|
52
|
+
/status(?:\s+code)?[\s:=]+401/i.test(text) ||
|
|
53
|
+
/401\s+Unauthorized/i.test(text);
|
|
54
|
+
const isFromZauru =
|
|
55
|
+
/zauru/i.test(text) ||
|
|
56
|
+
/\/apps\/webapp_tables\//i.test(text) ||
|
|
57
|
+
/your session expired/i.test(text);
|
|
58
|
+
|
|
59
|
+
if (isUnauthorized && isFromZauru) return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const hasZauruOauthReauthCookie = (
|
|
66
|
+
cookieHeader?: string | null,
|
|
67
|
+
): boolean =>
|
|
68
|
+
(cookieHeader ?? "")
|
|
69
|
+
.split(";")
|
|
70
|
+
.some(
|
|
71
|
+
(part) =>
|
|
72
|
+
part.trim() === `${ZAURU_OAUTH_REAUTH_COOKIE}=1`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export const createZauruSessionReauthPayload = (
|
|
76
|
+
overrides: ZauruSessionReauthPayloadOverrides = {},
|
|
77
|
+
): ZauruSessionReauthPayload => ({
|
|
78
|
+
error: true,
|
|
79
|
+
reauthRequired: true,
|
|
80
|
+
reauthUrl: overrides.reauthUrl ?? ZAURU_SESSION_REAUTH_URL,
|
|
81
|
+
title: overrides.title ?? "La sesión venció",
|
|
82
|
+
type: overrides.type ?? "info",
|
|
83
|
+
description:
|
|
84
|
+
overrides.description ??
|
|
85
|
+
"Inicie sesión nuevamente para continuar con la operación.",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const isZauruSessionReauthPayload = (
|
|
89
|
+
value: unknown,
|
|
90
|
+
): value is ZauruSessionReauthPayload =>
|
|
91
|
+
Boolean(
|
|
92
|
+
value &&
|
|
93
|
+
typeof value === "object" &&
|
|
94
|
+
(value as { reauthRequired?: unknown }).reauthRequired === true,
|
|
95
|
+
);
|