Directory-submission readiness: add MCP tool annotations (title + readOnlyHint/destructiveHint), /privacy policy route + README Privacy Policy section, bump to 2.3.2

This commit is contained in:
Aura CI
2026-09-08 12:57:08 +06:00
parent c25409586c
commit 7fe26ecc70
18 changed files with 528 additions and 324 deletions
+5 -4
View File
@@ -2,12 +2,13 @@ import { CourierAdapter } from "./base.js";
import { BalanceResponse, ParcelCreateRequest, ParcelResponse, SupportedCourier, TrackingResponse } from "../types.js";
export declare class PaperflyAdapter implements CourierAdapter {
courierName: SupportedCourier;
private apiKey;
private username;
private password;
private storeName;
private client;
private user;
private pass;
private key;
private enabled;
constructor(user: string, pass: string, key: string, baseUrl?: string);
constructor(apiKey: string, username: string, password: string, storeName: string, baseUrl: string);
isConfigured(): boolean;
createParcel(req: ParcelCreateRequest): Promise<ParcelResponse>;
trackParcel(trackingCode: string): Promise<TrackingResponse>;
+56 -45
View File
@@ -1,25 +1,22 @@
import axios from "axios";
export class PaperflyAdapter {
courierName = "paperfly";
apiKey;
username;
password;
storeName;
client;
user;
pass;
key;
enabled;
constructor(user, pass, key, baseUrl = "https://api.paperfly.com.bd") {
this.user = user;
this.pass = pass;
this.key = key;
this.enabled = Boolean(user && pass && key);
const token = Buffer.from(`${user}:${pass}`).toString("base64");
constructor(apiKey, username, password, storeName, baseUrl) {
this.apiKey = apiKey;
this.username = username;
this.password = password;
this.storeName = storeName;
this.enabled = Boolean(apiKey && username && password);
this.client = axios.create({
baseURL: baseUrl,
headers: {
Authorization: `Basic ${token}`,
pap_key: key,
"Content-Type": "application/json",
},
timeout: 10000,
timeout: 15000,
headers: { "Content-Type": "application/json" },
});
}
isConfigured() {
@@ -27,59 +24,73 @@ export class PaperflyAdapter {
}
async createParcel(req) {
if (!this.enabled) {
throw new Error("Paperfly Courier credentials (User / Pass / Key) are not configured.");
throw new Error("Paperfly credentials (paperflykey + merchant username & password) are not configured.");
}
if (!this.storeName) {
throw new Error("Paperfly store name is required to create a parcel.");
}
const payload = {
merOrderRef: req.invoice,
pickMerchantName: "Aura AI Commerce",
pickMerchantAddress: "Dhaka, Bangladesh",
custName: req.recipient_name,
custPhone: req.recipient_phone,
custAddr: req.recipient_address,
customerDistrict: "Dhaka",
customerThana: "Gulshan",
packagePrice: req.cod_amount,
max_weight: req.item_weight || 0.5,
productBrief: req.item_type || "General Ecommerce item",
deliveryOption: "regular",
merchantOrderReference: req.invoice,
storeName: this.storeName,
productBrief: req.item_type || "Product",
packagePrice: String(req.value ?? req.cod_amount ?? 0),
max_weight: String(req.item_weight ?? 0.5),
customerName: req.recipient_name,
customerAddress: req.recipient_address,
customerPhone: req.recipient_phone,
};
const response = await this.client.post("/OrderPlacement", payload);
const trackingCode = response.data?.tracking_number || req.invoice;
const response = await this.client.post("/merchant/api/service/new_order_v2.php", payload, {
headers: { paperflykey: this.apiKey },
auth: { username: this.username, password: this.password },
});
const data = response.data;
const ok = data?.success;
if (!ok?.tracking_number) {
throw new Error(`Paperfly API error: ${JSON.stringify(data?.error || data?.message || data)}`);
}
return {
success: true,
courier: "paperfly",
tracking_code: trackingCode,
consignment_id: trackingCode,
tracking_code: ok.tracking_number,
consignment_id: ok.tracking_barcode || ok.tracking_number,
invoice: req.invoice,
status: "order_placed",
status: ok.message || "created",
cod_amount: req.cod_amount,
created_at: new Date().toISOString(),
raw_response: response.data,
raw_response: data,
};
}
async trackParcel(trackingCode) {
if (!this.enabled) {
throw new Error("Paperfly Courier credentials are not configured.");
throw new Error("Paperfly credentials are not configured.");
}
const response = await this.client.post("/Tracking", {
ReferenceNumber: trackingCode,
});
if (!this.username || !this.password) {
throw new Error("Paperfly tracking needs your merchant-panel username & password (Basic Auth). Add them to use track_parcel.");
}
// Paperfly tracks by your merchant order reference (merchantOrderReference), not its tracking number.
const response = await this.client.post("/API-Order-Tracking", { ReferenceNumber: trackingCode }, { headers: { paperflykey: this.apiKey }, auth: { username: this.username, password: this.password } });
const data = response.data;
const st = data?.success?.trackingStatus?.[0] || {};
const stages = [
[st.Delivered, "delivered"],
[st.Partial, "partial-delivery"],
[st.Returned, "returned"],
[st.PickedForDelivery, "out-for-delivery"],
[st.inTransit, "in-transit"],
[st.ReceivedAtPoint, "received-at-point"],
[st.Pick, "picked-up"],
];
const status = stages.find(([v]) => v && String(v).trim())?.[1] || data?.success?.message || "pending";
return {
success: true,
courier: "paperfly",
tracking_code: trackingCode,
status: data?.Status || "in_progress",
status,
updated_at: new Date().toISOString(),
raw_response: data,
};
}
async getBalance() {
return {
success: true,
courier: "paperfly",
current_balance: 0,
raw_response: { message: "Paperfly merchant settlements processed weekly via Wingman portal." },
};
throw new Error("Paperfly does not expose a merchant balance endpoint via its public API — check the Paperfly merchant panel.");
}
}
+5 -5
View File
@@ -1,14 +1,14 @@
import { CourierAdapter } from "./base.js";
import { BalanceResponse, LocationResolutionResponse, ParcelCreateRequest, ParcelResponse, SupportedCourier, TrackingResponse } from "../types.js";
export declare class RedXAdapter implements CourierAdapter {
import { BalanceResponse, ParcelCreateRequest, ParcelResponse, SupportedCourier, TrackingResponse } from "../types.js";
export declare class RedxAdapter implements CourierAdapter {
courierName: SupportedCourier;
private client;
private apiToken;
private enabled;
constructor(apiToken: string, baseUrl?: string);
private pickupStoreId;
constructor(apiToken: string, baseUrl: string, pickupStoreId?: string);
isConfigured(): boolean;
private resolveDeliveryArea;
createParcel(req: ParcelCreateRequest): Promise<ParcelResponse>;
trackParcel(trackingCode: string): Promise<TrackingResponse>;
getBalance(): Promise<BalanceResponse>;
getLocations(cityName?: string): Promise<LocationResolutionResponse>;
}
+62 -45
View File
@@ -1,94 +1,111 @@
import axios from "axios";
export class RedXAdapter {
export class RedxAdapter {
courierName = "redx";
client;
apiToken;
enabled;
constructor(apiToken, baseUrl = "https://openapi.redx.com.bd/v1.0.0-beta") {
this.apiToken = apiToken;
pickupStoreId;
constructor(apiToken, baseUrl, pickupStoreId = "") {
this.enabled = Boolean(apiToken);
this.pickupStoreId = pickupStoreId;
this.client = axios.create({
baseURL: baseUrl,
headers: {
Authorization: `Bearer ${apiToken}`,
"API-ACCESS-TOKEN": `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
timeout: 10000,
timeout: 15000,
});
}
isConfigured() {
return this.enabled;
}
// RedX's create endpoint needs a numeric delivery_area_id. If the caller did
// not pass one, best-effort resolve it from the address via GET /areas.
async resolveDeliveryArea(req) {
if (req.delivery_area_id) {
return { id: Number(req.delivery_area_id), name: req.delivery_area || "" };
}
const res = await this.client.get("/areas");
const areas = res.data?.areas || [];
const addr = (req.recipient_address || "").toLowerCase();
let best = null;
for (const a of areas) {
const base = String(a.name).split("(")[0].trim().toLowerCase();
if (base && addr.includes(base)) {
const bestLen = best ? String(best.name).split("(")[0].trim().length : 0;
if (base.length > bestLen)
best = a;
}
}
if (!best) {
throw new Error("RedX: could not match a delivery area from the address. Pass 'delivery_area_id' explicitly (look it up via RedX /areas).");
}
return { id: best.id, name: best.name };
}
async createParcel(req) {
if (!this.enabled) {
throw new Error("RedX Courier credentials (API Token) are not configured.");
throw new Error("RedX credentials (API access token) are not configured.");
}
const area = await this.resolveDeliveryArea(req);
const weightGrams = Math.max(1, Math.round((req.item_weight ?? 0.5) * 1000));
const declaredValue = String(req.value ?? req.cod_amount ?? 0);
const payload = {
customer_name: req.recipient_name,
customer_phone: req.recipient_phone,
delivery_area: req.recipient_address,
delivery_area_id: 1, // Default auto-resolved area
delivery_area: area.name || req.delivery_area || "",
delivery_area_id: area.id,
customer_address: req.recipient_address,
merchant_invoice_id: req.invoice,
cash_collection_amount: req.cod_amount,
parcel_weight: (req.item_weight || 0.5) * 1000, // RedX uses grams
cash_collection_amount: String(req.cod_amount ?? 0),
parcel_weight: weightGrams,
instruction: req.note || "Aura AI automated dispatch",
value: req.cod_amount || 500,
value: declaredValue,
};
const response = await this.client.post("/parcels", payload);
const trackingId = response.data.tracking_id || response.data.parcel_id;
const storeId = req.pickup_store_id || this.pickupStoreId;
if (storeId)
payload.pickup_store_id = Number(storeId);
if (req.item_type) {
payload.parcel_details_json = [
{ name: req.item_type, category: req.item_category || "general", value: Number(req.value ?? req.cod_amount ?? 0) },
];
}
const response = await this.client.post("/parcel", payload);
const data = response.data;
const trackingId = data?.tracking_id;
if (!trackingId) {
throw new Error(`RedX API error: ${JSON.stringify(data?.message || data)}`);
}
return {
success: true,
courier: "redx",
tracking_code: trackingId,
consignment_id: trackingId,
invoice: req.invoice,
status: "ready_for_pickup",
status: "pickup-pending",
cod_amount: req.cod_amount,
delivery_fee: response.data.delivery_fee || 60,
created_at: new Date().toISOString(),
raw_response: response.data,
raw_response: data,
};
}
async trackParcel(trackingCode) {
if (!this.enabled) {
throw new Error("RedX Courier credentials are not configured.");
throw new Error("RedX credentials are not configured.");
}
const response = await this.client.get(`/parcels/track/${encodeURIComponent(trackingCode)}`);
const response = await this.client.get(`/parcel/track/${encodeURIComponent(trackingCode)}`);
const data = response.data;
const events = data?.tracking || [];
const latest = events[events.length - 1];
return {
success: true,
courier: "redx",
tracking_code: trackingCode,
status: data.tracking?.status || "in_transit",
timeline: (data.tracking?.history || []).map((h) => ({
status: h.status,
time: h.created_at,
note: h.message,
})),
status: latest?.message_en || "unknown",
updated_at: latest?.time || new Date().toISOString(),
timeline: events.map((e) => ({ status: e.message_en || "", time: e.time || "", note: e.message_bn })),
raw_response: data,
};
}
async getBalance() {
return {
success: true,
courier: "redx",
current_balance: 0,
raw_response: { message: "RedX payouts are disbursed directly to connected merchant bank accounts." },
};
}
async getLocations(cityName) {
const response = await this.client.get("/areas");
const areas = response.data.areas || [];
return {
success: true,
courier: "redx",
locations: areas.map((a) => ({
city_id: a.id,
city_name: a.name,
zone_name: a.zone_name,
area_name: a.name,
})),
};
throw new Error("RedX does not expose a merchant balance endpoint via its public API — check your balance in the RedX merchant panel.");
}
}