@meuecommerce/frete-adapter-node 0.1.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/dist/correiosHttpClient.d.ts +30 -0
- package/dist/correiosHttpClient.js +116 -0
- package/dist/envSecretStore.d.ts +22 -0
- package/dist/envSecretStore.js +15 -0
- package/dist/inMemorySettingsStore.d.ts +19 -0
- package/dist/inMemorySettingsStore.js +20 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/package.json +31 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node implementation of the `CorreiosClient` port using global `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Ports the retry/backoff behavior from the Velo backend:
|
|
5
|
+
* - `auth-methods.js` -> `authenticate` (up to 5 tries, fail fast on 4xx)
|
|
6
|
+
* - `estimate-methods.js` -> `getPrice`/`getTime` (timeout + 2 retries, backoff)
|
|
7
|
+
*
|
|
8
|
+
* The only host dependency is `fetch`, injectable for tests.
|
|
9
|
+
*/
|
|
10
|
+
import { type CorreiosClient } from "@meuecommerce/frete";
|
|
11
|
+
type FetchLike = typeof globalThis.fetch;
|
|
12
|
+
export interface CorreiosHttpClientOptions {
|
|
13
|
+
/** Injectable fetch (defaults to global fetch). */
|
|
14
|
+
fetch?: FetchLike;
|
|
15
|
+
/** Correios API base URL (defaults to the production URL). */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
/** Per-request timeout for price/time calls, ms. Default 20000. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
/** Max attempts for price/time calls. Default 2. */
|
|
20
|
+
estimateRetries?: number;
|
|
21
|
+
/** Base backoff between price/time retries, ms. Default 500 (exponential). */
|
|
22
|
+
estimateRetryDelayMs?: number;
|
|
23
|
+
/** Max attempts for authentication. Default 5. */
|
|
24
|
+
authRetries?: number;
|
|
25
|
+
/** Fixed delay between auth retries, ms. Default 1000. */
|
|
26
|
+
authRetryDelayMs?: number;
|
|
27
|
+
logger?: Pick<Console, "warn" | "error" | "log">;
|
|
28
|
+
}
|
|
29
|
+
export declare function createCorreiosHttpClient(options?: CorreiosHttpClientOptions): CorreiosClient;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node implementation of the `CorreiosClient` port using global `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Ports the retry/backoff behavior from the Velo backend:
|
|
5
|
+
* - `auth-methods.js` -> `authenticate` (up to 5 tries, fail fast on 4xx)
|
|
6
|
+
* - `estimate-methods.js` -> `getPrice`/`getTime` (timeout + 2 retries, backoff)
|
|
7
|
+
*
|
|
8
|
+
* The only host dependency is `fetch`, injectable for tests.
|
|
9
|
+
*/
|
|
10
|
+
import { correiosApiUrl, } from "@meuecommerce/frete";
|
|
11
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
function base64(input) {
|
|
13
|
+
return Buffer.from(input, "utf-8").toString("base64");
|
|
14
|
+
}
|
|
15
|
+
export function createCorreiosHttpClient(options = {}) {
|
|
16
|
+
const { fetch = globalThis.fetch, baseUrl = correiosApiUrl, timeoutMs = 20000, estimateRetries = 2, estimateRetryDelayMs = 500, authRetries = 5, authRetryDelayMs = 1000, logger = console, } = options;
|
|
17
|
+
if (typeof fetch !== "function") {
|
|
18
|
+
throw new Error("createCorreiosHttpClient: no fetch available (Node 18+ or pass options.fetch)");
|
|
19
|
+
}
|
|
20
|
+
/** Run a fetch with a hard timeout + exponential-backoff retry. */
|
|
21
|
+
async function fetchWithRetry(url, init) {
|
|
22
|
+
let lastError;
|
|
23
|
+
for (let attempt = 0; attempt < estimateRetries; attempt++) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
|
+
try {
|
|
27
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
lastError = error;
|
|
31
|
+
if (attempt === estimateRetries - 1)
|
|
32
|
+
break;
|
|
33
|
+
await sleep(estimateRetryDelayMs * Math.pow(2, attempt));
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw lastError;
|
|
40
|
+
}
|
|
41
|
+
async function estimate(path, payload, token, tag) {
|
|
42
|
+
const url = `${baseUrl}${path}`;
|
|
43
|
+
const init = {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
Accept: "application/json",
|
|
48
|
+
Authorization: `Bearer ${token}`,
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(payload),
|
|
51
|
+
};
|
|
52
|
+
const response = await fetchWithRetry(url, init);
|
|
53
|
+
if (response.status === 401) {
|
|
54
|
+
throw { status: 401, message: "unauthorized" };
|
|
55
|
+
}
|
|
56
|
+
if (response.status !== 200 && response.status !== 206) {
|
|
57
|
+
// Correios returns a structured error body on bad requests; the domain
|
|
58
|
+
// layer filters these out by `txErro`, so pass it through unchanged.
|
|
59
|
+
const json = await response.json().catch(() => ({}));
|
|
60
|
+
logger.warn(`[${tag}] invalid response. status=${response.status} body=${JSON.stringify(json)}`);
|
|
61
|
+
return json;
|
|
62
|
+
}
|
|
63
|
+
return (await response.json());
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
async authenticate(credentials) {
|
|
67
|
+
const url = `${baseUrl}/token/v1/autentica/cartaopostagem`;
|
|
68
|
+
let lastError;
|
|
69
|
+
for (let attempt = 0; attempt < authRetries; attempt++) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
accept: "application/json",
|
|
76
|
+
Authorization: `Basic ${base64(`${credentials.user}:${credentials.apiKey}`)}`,
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({ numero: credentials.postcard }),
|
|
79
|
+
});
|
|
80
|
+
// 4xx come from bad credentials/contract, not transient failures —
|
|
81
|
+
// fail fast instead of burning retries (and seconds) on them.
|
|
82
|
+
if (response.status >= 400 && response.status < 500) {
|
|
83
|
+
throw {
|
|
84
|
+
status: response.status,
|
|
85
|
+
message: response.status === 401
|
|
86
|
+
? "Erro de autenticação nos correios."
|
|
87
|
+
: "Não foi possível autenticar o contrato nos Correios.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return (await response.json());
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const status = error?.status;
|
|
94
|
+
if (typeof status === "number" && status >= 400 && status < 500)
|
|
95
|
+
throw error;
|
|
96
|
+
lastError = error;
|
|
97
|
+
if (attempt < authRetries - 1) {
|
|
98
|
+
await sleep(authRetryDelayMs);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// apiKey intentionally omitted from logs — it is a secret.
|
|
102
|
+
logger.error("[correios.authenticate]:", error, credentials.user, credentials.postcard);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw lastError;
|
|
108
|
+
},
|
|
109
|
+
getPrice(payload, token) {
|
|
110
|
+
return estimate("/preco/v1/nacional", payload, token, "correios.getPrice");
|
|
111
|
+
},
|
|
112
|
+
getTime(payload, token) {
|
|
113
|
+
return estimate("/prazo/v1/nacional", payload, token, "correios.getTime");
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SecretStore` backed by environment variables — the Node/Fly and MCP-server
|
|
3
|
+
* equivalent of Wix Secrets Manager.
|
|
4
|
+
*
|
|
5
|
+
* Secret names are normalized to an env-var convention: uppercased with
|
|
6
|
+
* non-alphanumerics turned into underscores, plus an optional prefix. So
|
|
7
|
+
* `getSecret("backfill_secret")` reads `MEUFRETE_BACKFILL_SECRET` by default.
|
|
8
|
+
*/
|
|
9
|
+
import type { SecretStore } from "@meuecommerce/frete";
|
|
10
|
+
export interface EnvSecretStoreOptions {
|
|
11
|
+
/** Env source (defaults to process.env). */
|
|
12
|
+
env?: Record<string, string | undefined>;
|
|
13
|
+
/** Prefix applied to normalized names. Default "MEUFRETE_". Pass "" for none. */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class EnvSecretStore implements SecretStore {
|
|
17
|
+
private readonly env;
|
|
18
|
+
private readonly prefix;
|
|
19
|
+
constructor(options?: EnvSecretStoreOptions);
|
|
20
|
+
private key;
|
|
21
|
+
getSecret(name: string): Promise<string | null>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class EnvSecretStore {
|
|
2
|
+
env;
|
|
3
|
+
prefix;
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.env = options.env ?? process.env;
|
|
6
|
+
this.prefix = options.prefix ?? "MEUFRETE_";
|
|
7
|
+
}
|
|
8
|
+
key(name) {
|
|
9
|
+
return this.prefix + name.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
async getSecret(name) {
|
|
12
|
+
const value = this.env[this.key(name)];
|
|
13
|
+
return value == null || value === "" ? null : value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `SettingsStore` — for local dev, the MCP server's local mode, and
|
|
3
|
+
* tests. A real deployment swaps this for a Prisma- or wixData-backed store; the
|
|
4
|
+
* core and the MCP tools don't change.
|
|
5
|
+
*/
|
|
6
|
+
import type { DefaultSettings, MerchantSettings, SettingsStore } from "@meuecommerce/frete";
|
|
7
|
+
export interface InMemorySettingsStoreOptions {
|
|
8
|
+
defaults: DefaultSettings;
|
|
9
|
+
merchants?: Record<string, MerchantSettings>;
|
|
10
|
+
}
|
|
11
|
+
export declare class InMemorySettingsStore implements SettingsStore {
|
|
12
|
+
private readonly defaults;
|
|
13
|
+
private readonly merchants;
|
|
14
|
+
constructor(options: InMemorySettingsStoreOptions);
|
|
15
|
+
/** Add or replace a merchant's settings. */
|
|
16
|
+
set(instanceId: string, settings: MerchantSettings): void;
|
|
17
|
+
getMerchantSettings(instanceId: string): Promise<MerchantSettings | null>;
|
|
18
|
+
getDefaultSettings(): Promise<DefaultSettings>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class InMemorySettingsStore {
|
|
2
|
+
defaults;
|
|
3
|
+
merchants = new Map();
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.defaults = options.defaults;
|
|
6
|
+
for (const [id, settings] of Object.entries(options.merchants ?? {})) {
|
|
7
|
+
this.merchants.set(id, settings);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Add or replace a merchant's settings. */
|
|
11
|
+
set(instanceId, settings) {
|
|
12
|
+
this.merchants.set(instanceId, settings);
|
|
13
|
+
}
|
|
14
|
+
async getMerchantSettings(instanceId) {
|
|
15
|
+
return this.merchants.get(instanceId) ?? null;
|
|
16
|
+
}
|
|
17
|
+
async getDefaultSettings() {
|
|
18
|
+
return this.defaults;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @meuecommerce/frete-adapter-node — Node implementations of @meuecommerce/frete ports.
|
|
3
|
+
*
|
|
4
|
+
* Wraps global `fetch` so the shipping core can run outside Velo: on the
|
|
5
|
+
* Shopify/Fly deployment today, and the MCP server next.
|
|
6
|
+
*/
|
|
7
|
+
export { createCorreiosHttpClient } from "./correiosHttpClient.js";
|
|
8
|
+
export type { CorreiosHttpClientOptions } from "./correiosHttpClient.js";
|
|
9
|
+
export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
|
|
10
|
+
export type { InMemorySettingsStoreOptions } from "./inMemorySettingsStore.js";
|
|
11
|
+
export { EnvSecretStore } from "./envSecretStore.js";
|
|
12
|
+
export type { EnvSecretStoreOptions } from "./envSecretStore.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @meuecommerce/frete-adapter-node — Node implementations of @meuecommerce/frete ports.
|
|
3
|
+
*
|
|
4
|
+
* Wraps global `fetch` so the shipping core can run outside Velo: on the
|
|
5
|
+
* Shopify/Fly deployment today, and the MCP server next.
|
|
6
|
+
*/
|
|
7
|
+
export { createCorreiosHttpClient } from "./correiosHttpClient.js";
|
|
8
|
+
export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
|
|
9
|
+
export { EnvSecretStore } from "./envSecretStore.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meuecommerce/frete-adapter-node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node implementation of @meuecommerce/frete ports (Correios HTTP client) using global fetch. Used by the Shopify/Fly deployment and the MCP server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@meuecommerce/frete": "^0.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|