Codex/sdk production baseline#1
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a production baseline for the Codex/SDK, focusing on enhancing the Ecommaps client with new features, improved type safety, and consistent data handling while ensuring backward compatibility. It provides developers with more flexibility in configuring the client and accessing store data. Highlights
Changelog
Ignored Files
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Free Tier Details
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Search query parameter silently overridden by params spread
- I changed the merge order in
products.searchto{ ...(params || {}), q }so the explicitqargument always takes precedence over anyparams.qvalue.
- I changed the merge order in
Or push these changes by commenting:
@cursor push 78596dfa8c
Preview (78596dfa8c)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -1,0 +1,36 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.28.0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+
+ - name: Install
+ run: pnpm install --frozen-lockfile
+
+ - name: Typecheck
+ run: pnpm typecheck
+
+ - name: Build
+ run: pnpm build
+
+ - name: Package smoke test
+ run: npm pack --dry-run
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -1,0 +1,40 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.28.0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ registry-url: https://registry.npmjs.org
+ cache: pnpm
+
+ - name: Install
+ run: pnpm install --frozen-lockfile
+
+ - name: Build
+ run: pnpm build
+
+ - name: Publish to npm
+ run: npm publish --access public --provenance
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
--- /dev/null
+++ b/.gitignore
@@ -1,0 +1,8 @@
+node_modules/
+dist/
+.env
+.env.*
+*.log
+.DS_Store
+coverage/
+.npmrc
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -1,0 +1,16 @@
+# Changelog:
+
+## 1.4.0
+
+- Added `createEcommapsClient(config)` factory.
+- Added typed Store endpoints:
+ - `store.pages.list/retrieve`
+ - `store.blogs.list/retrieve`
+ - `store.coupons.validate` (typed payload/response)
+- Improved type coverage for:
+ - product variants and option values
+ - product/media payloads
+ - promotions/coupons
+ - customer addresses
+- Normalized list payload handling (`[]` and `{ data: [] }`) inside client methods.
+- Kept `ecommapsClient` default export behavior (non-breaking for existing apps).
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
--- /dev/null
+++ b/MIGRATION.md
@@ -1,0 +1,16 @@
+# Migration Notes (v1.4.x)
+
+`@ecommaps/client` remains backward compatible while adding new typed capabilities.
+
+## New in v1.4.x
+
+- `createEcommapsClient(config)`
+- `store.pages.list/retrieve`
+- `store.blogs.list/retrieve`
+- typed `store.coupons.validate`
+- stronger product/variant/promotion/page/blog types
+
+## No breaking changes
+
+- Existing `ecommapsClient` import still works.
+- Existing cart/auth/products/orders calls keep the same usage.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,144 +1,114 @@
# @ecommaps/client
-[](https://www.npmjs.com/package/@ecommaps/client)
-[](https://opensource.org/licenses/ISC)
+Official TypeScript SDK for Ecommaps storefront APIs.
-The official, high-performance JavaScript/TypeScript SDK for building custom, headless storefronts on the **Ecommaps** ecosystem. Specifically tailored for the **Algerian (DZ)**, **North African (MENA)**, and **Middle Eastern** e-commerce markets. Designed for developers and AI agents to build, scale, and manage modern commerce experiences with sub-second performance and local feature support (DZD currency, local addresses, and region-specific logic).
+## 1. Positioning
----
+`@ecommaps/client` is the Core API SDK layer.
-## 🚀 Quick Start
+Use this package when you need:
+- direct access to Storefront endpoints
+- strongly typed contracts for products, cart, auth, pages, blogs, and coupons
+- environment-agnostic API consumption in Next.js, Node.js, Edge, or workers
-### Installation
+If you need commerce logic (variant resolver, promotion classifier), use `@ecommaps/storefront-kit`.
+## 2. Installation
+
```bash
-npm install @ecommaps/client
-# or
pnpm add @ecommaps/client-### Configuration
+## 3. Configuration
-The client automatically reads from environment variables but can also be configured per request.
+### Environment variables
-```typescript
-import { ecommapsClient } from "@ecommaps/client";
-// Set these in your .env
-// NEXT_PUBLIC_ECOMMAPS_API_URL=https://api.ecommaps.com/api/v1/storefront
-// ECOMMAPS_API_KEY=your_store_key
+```env
+NEXT_PUBLIC_ECOMMAPS_API_URL=https://api.ecommaps.com/api/v1/storefront
+ECOMMAPS_API_KEY=your_storefront_api_key
----
+### Default singleton client
-## 🛠 Features & Examples
+```ts
+import { ecommapsClient } from "@ecommaps/client";
+```
-### 🔐 Authentication & Customer Profile
-Handle customer sessions using industry-standard JWT.
+### Factory client
-```typescript
-// Login
-const { token, user } = await ecommapsClient.auth.login({
- email: "customer@example.com",
- password: "secure_password"
-});
+```ts
+import { createEcommapsClient } from "@ecommaps/client";
-// Get Current User Profile (Authenticated)
-const { customer } = await ecommapsClient.auth.me({
- headers: { Authorization: `Bearer ${token}` }
+const client = createEcommapsClient({
+ apiUrl: "https://api.ecommaps.com/api/v1/storefront",
+ apiKey: process.env.ECOMMAPS_API_KEY!,
});
-
-// Add Address to Customer Profile
-await ecommapsClient.auth.addAddress({
- line1: "123 Main St",
- city: "Algiers",
- state: "16",
- country: "DZ",
- postal_code: "16000",
- phone: "0555000000",
- label: "Home"
-}, {
- headers: { Authorization: `Bearer ${token}` }
-});
-### 📦 Products & Collections
-Retrieve catalog data with built-in pagination.
+## 4. API Surface
-```typescript
-// List Products
-const products = await ecommapsClient.products.list({ limit: 10, offset: 0 });
+### Store
-// Retrieve Single Product by Slug
-const product = await ecommapsClient.products.retrieve("summer-t-shirt");
-// List Collections
-const { data: collections } = await ecommapsClient.collections.list();
+```ts
+await ecommapsClient.store.retrieve();
+await ecommapsClient.store.menus.list();
+await ecommapsClient.store.menus.retrieve("main-menu");
+await ecommapsClient.store.pages.list();
+await ecommapsClient.store.pages.retrieve("refund");
+await ecommapsClient.store.blogs.list();
+await ecommapsClient.store.blogs.retrieve("new-arrivals");
+await ecommapsClient.store.coupons.validate({ code: "DISCOUNT20", cart_total: 10000, items: [] });
-### 🛒 Cart Management
-Highly flexible cart APIs optimized for sub-second performance.
+### Products / Collections
-```typescript
-// Create a new Cart
-const cart = await ecommapsClient.cart.create();
-
-// Add Item
-await ecommapsClient.cart.addItem(cart.id, {
- product_id: "prod_123",
- quantity: 2
-});
-
-// Retrieve Cart
-const currentCart = await ecommapsClient.cart.retrieve(cart.id);
+```ts
+await ecommapsClient.products.list({ limit: 12, offset: 0 });
+await ecommapsClient.products.retrieve("product-slug");
+await ecommapsClient.products.search("shirt", { limit: 10 });
+await ecommapsClient.collections.list();
+await ecommapsClient.collections.retrieve("menswear", 20, 0);
-### 🧾 Orders
-Seamless order management from creation to tracking.
+### Cart / Orders / Auth
-```typescript
-// List Customer Orders (Authenticated)
-const { data: orders } = await ecommapsClient.orders.list({
- headers: { Authorization:
Bearer ${token}}, - params: { limit: 5 }
-});
-// Retrieve Order Details
-const order = await ecommapsClient.orders.retrieve("ECORD-2024-XXXX");
+```ts
+const cart = await ecommapsClient.cart.create();
+await ecommapsClient.cart.addItem(cart.id, { product_id: "product_uuid", variant_id: "variant_uuid", quantity: 1 });
+await ecommapsClient.cart.retrieve(cart.id);
+await ecommapsClient.orders.create({ cart_id: cart.id, customer_name: "Name", customer_phone: "0555000000" });
+await ecommapsClient.auth.login({ email: "user@mail.com", password: "secret" });
----
+## 5. Error handling
-## 🤖 AI Agent Integration (Skills)
+All non-2xx responses throw `EcommapsAPIError`:
-This SDK is **Agent-Ready**. If you are an AI agent building for a user, follow these conventions:
+```ts
+import { EcommapsAPIError } from "@ecommaps/client";
-1. **Mapping**: Always map the backend JSON structure to the UI components (e.g., Use `line1` for `address_1`, `name` for `title`).
-2. **Context**: Use `ecommapsClient.auth.me` to verify user status before attempting protected operations.
-3. **Optimistic UI**: Use the returned data from cart/order operations to update local state immediately.
+try {
+ await ecommapsClient.products.retrieve("missing");
+} catch (error) {
+ if (error instanceof EcommapsAPIError) {
+ console.error(error.status, error.message);
+ }
+}
+```
----
+## 6. Compatibility
-## 🏗 Developing the Storefront
+- Runtime: Node.js 20+
+- Module formats: ESM + CJS
+- TypeScript: full declaration files included
+- Backward compatibility: `ecommapsClient` default singleton remains supported
-To build a complete store:
-1. Initialize a Next.js 14+ project.
-2. Install `@ecommaps/client`.
-3. Use Next.js **Server Actions** to wrap SDK calls (see `auth` and `cart`).
-4. Implement **Dynamic Routing** (`/[slug]`) using `products.retrieve`.
+## 7. Migration
----
+See [MIGRATION.md](./MIGRATION.md).
-## 🌍 Universal Compatibility
+## 8. Release policy
-The Ecommaps SDK is built using standard Fetch API and is environment-agnostic. It works seamlessly across:
-- **Web Frameworks**: React, Next.js, Vue, Nuxt, Svelte, Angular.
-- **Mobile**: React Native, Expo, Ionic.
-- **Server-side**: Node.js, Bun, Deno, Edge Functions (Vercel, Cloudflare).
-- **AI Agents**: Highly optimized for AI-driven development and autonomous agents.
-
----
-
-## 📄 License
-
-Distributed under the ISC License. © 2026 ecommaps.com
+- Semver is enforced.
+- Breaking changes are released only in major versions.
+- Every published change must include changelog notes.
diff --git a/dist/index.d.mts b/dist/index.d.mts
deleted file mode 100644
--- a/dist/index.d.mts
+++ /dev/null
@@ -1,228 +1,0 @@
-interface EcommapsMenuItem {
- title: string;
- url: string | null;
- type?: string | null;
- position?: number;
- children?: EcommapsMenuItem[];
-}
-interface EcommapsMenu {
- id: string;
- title: string;
- handle: string;
- items: EcommapsMenuItem[];
-}
-interface EcommapsSite {
- id: string;
- name: string;
- slug: string;
- description: string | null;
- logo_url: string | null;
- custom_domain: string | null;
- theme: any | null;
- theme_settings: any | null;
- settings: any | null;
- contact_info: any | null;
- social_links: any | null;
-}
-interface EcommapsProductVariant {
- title: string;
- prices: any[];
- options: any[];
- inventory_quantity: number;
- sku: string | null;
- id?: string;
-}
-interface EcommapsProduct {
- id: string;
- site_id: string;
- name: string;
- slug: string;
- description: string | null;
- price: number;
- compare_at_price: number | null;
- currency: string | null;
- images: any | null;
- category: string | null;
- tags: string[] | null;
- inventory_quantity: number | null;
- is_active: boolean | null;
- created_at: string | null;
- updated_at: string | null;
- sku: string | null;
- barcode: string | null;
- cost_per_item: number | null;
- track_quantity: boolean | null;
- continue_selling_when_out_of_stock: boolean | null;
- weight: number | null;
- weight_unit: string | null;
- vendor: string | null;
- product_type: string | null;
- options: any | null;
- variants: any | null;
- charge_tax: boolean | null;
- is_physical: boolean | null;
- discounted_price?: number;
- discount_text?: string;
-}
-interface EcommapsCollection {
- id: string;
- site_id: string;
- title: string;
- description: string | null;
- slug: string;
- image_url: string | null;
- is_active: boolean | null;
- sort_order: string | null;
- created_at: string;
- updated_at: string;
-}
-interface PaginationMeta {
- total: number;
- limit: number;
- offset: number;
- has_more: boolean;
-}
-interface PaginatedResponse<T> {
- data: T[];
- pagination: PaginationMeta;
-}
-interface EcommapsCartItem {
- id: string;
- product_id: string;
- variant_id: string | null;
- product_name: string;
- product_price: number;
- product_image: string | null;
- quantity: number;
- subtotal: number;
-}
-interface EcommapsCart {
- id: string;
- site_id: string;
- items: EcommapsCartItem[];
- items_count: number;
- subtotal: number;
- created_at: string | null;
-}
-interface EcommapsCustomer {
- id: string;
- store_id: string;
- email: string;
- full_name: string;
- phone: string | null;
- addresses: any[];
- created_at: string;
-}
-interface EcommapsAuthResponse {
- token: string;
- user: EcommapsCustomer;
-}
-interface EcommapsSearchResponse {
- data: EcommapsProduct[];
- query: string;
- pagination: PaginationMeta;
-}
-interface EcommapsAppliedDiscount {
- id?: string;
- code?: string;
- discount_type?: "percentage" | "fixed_amount" | "free_shipping";
- discount_value?: number;
- discount_amount?: number;
- promotion_type?: string;
- target_type?: string;
- message?: string;
-}
-interface EcommapsCouponValidateResponse {
- valid: boolean;
- applied_discounts?: EcommapsAppliedDiscount[];
- code?: string;
- discount_type?: "percentage" | "fixed_amount" | "free_shipping";
- discount_value?: number;
- discount_amount?: number;
- target_type?: string;
- promotion_type?: string;
- message?: string;
-}
-
-declare class EcommapsAPIError extends Error {
- status: number;
- constructor(message: string, status: number);
-}
-declare const ecommapsClient: {
- store: {
- retrieve: (options?: RequestInit) => Promise<EcommapsSite>;
- menus: {
- list: (options?: RequestInit) => Promise<EcommapsMenu[]>;
- retrieve: (handle: string, options?: RequestInit) => Promise<EcommapsMenu>;
- };
- coupons: {
- validate: (body: {
- code: string;
- cart_total?: number;
- items?: any[];
- }, options?: RequestInit) => Promise<EcommapsCouponValidateResponse>;
- };
- };
- products: {
- list: (params?: Record<string, string | number>, options?: RequestInit) => Promise<PaginatedResponse<EcommapsProduct>>;
- retrieve: (slug: string, options?: RequestInit) => Promise<EcommapsProduct>;
- search: (q: string, params?: Record<string, string | number>, options?: RequestInit) => Promise<EcommapsSearchResponse>;
- };
- collections: {
- list: (options?: RequestInit) => Promise<{
- data: EcommapsCollection[];
- }>;
- retrieve: (slug: string, limit?: number, offset?: number, options?: RequestInit) => Promise<{
- collection: EcommapsCollection;
- products: EcommapsProduct[];
- pagination: any;
- }>;
- };
- cart: {
- create: (options?: RequestInit) => Promise<EcommapsCart>;
- retrieve: (cartId: string, options?: RequestInit) => Promise<EcommapsCart>;
- addItem: (cartId: string, body: {
- product_id: string;
- variant_id?: string;
- quantity: number;
- }, options?: RequestInit) => Promise<EcommapsCart>;
- updateItem: (cartId: string, itemId: string, body: {
- quantity: number;
- }, options?: RequestInit) => Promise<EcommapsCart>;
- removeItem: (cartId: string, itemId: string, options?: RequestInit) => Promise<EcommapsCart>;
- };
- orders: {
- create: (body: any, options?: RequestInit) => Promise<any>;
- retrieve: (orderNumber: string, options?: RequestInit) => Promise<any>;
- list: (options?: RequestInit & {
- params?: {
- limit?: number;
- offset?: number;
- };
- }) => Promise<{
- data: any[];
- pagination: any;
- }>;
- };
- auth: {
- login: (body: any, options?: RequestInit) => Promise<EcommapsAuthResponse>;
- signup: (body: any, options?: RequestInit) => Promise<EcommapsAuthResponse>;
- me: (options?: RequestInit) => Promise<{
- customer: EcommapsCustomer;
- }>;
- addAddress: (body: any, options?: RequestInit) => Promise<{
- success: boolean;
- address: any;
- }>;
- setDefaultAddress: (addressId: string, options?: RequestInit) => Promise<{
- success: boolean;
- addresses: any[];
- }>;
- deleteAddress: (addressId: string, options?: RequestInit) => Promise<{
- success: boolean;
- addresses: any[];
- }>;
- };
-};
-
-export { EcommapsAPIError, type EcommapsAppliedDiscount, type EcommapsAuthResponse, type EcommapsCart, type EcommapsCartItem, type EcommapsCollection, type EcommapsCouponValidateResponse, type EcommapsCustomer, type EcommapsMenu, type EcommapsMenuItem, type EcommapsProduct, type EcommapsProductVariant, type EcommapsSearchResponse, type EcommapsSite, type PaginatedResponse, type PaginationMeta, ecommapsClient };
\ No newline at end of file
diff --git a/dist/index.d.ts b/dist/index.d.ts
deleted file mode 100644
--- a/dist/index.d.ts
+++ /dev/null
@@ -1,228 +1,0 @@
-interface EcommapsMenuItem {
- title: string;
- url: string | null;
- type?: string | null;
- position?: number;
- children?: EcommapsMenuItem[];
-}
-interface EcommapsMenu {
- id: string;
- title: string;
- handle: string;
- items: EcommapsMenuItem[];
-}
-interface EcommapsSite {
- id: string;
- name: string;
- slug: string;
- description: string | null;
- logo_url: string | null;
- custom_domain: string | null;
- theme: any | null;
- theme_settings: any | null;
- settings: any | null;
- contact_info: any | null;
- social_links: any | null;
-}
-interface EcommapsProductVariant {
- title: string;
- prices: any[];
- options: any[];
- inventory_quantity: number;
- sku: string | null;
- id?: string;
-}
-interface EcommapsProduct {
- id: string;
- site_id: string;
- name: string;
- slug: string;
- description: string | null;
- price: number;
- compare_at_price: number | null;
- currency: string | null;
- images: any | null;
- category: string | null;
- tags: string[] | null;
- inventory_quantity: number | null;
- is_active: boolean | null;
- created_at: string | null;
- updated_at: string | null;
- sku: string | null;
- barcode: string | null;
- cost_per_item: number | null;
- track_quantity: boolean | null;
- continue_selling_when_out_of_stock: boolean | null;
- weight: number | null;
- weight_unit: string | null;
- vendor: string | null;
- product_type: string | null;
- options: any | null;
- variants: any | null;
- charge_tax: boolean | null;
- is_physical: boolean | null;
- discounted_price?: number;
- discount_text?: string;
-}
-interface EcommapsCollection {
- id: string;
- site_id: string;
- title: string;
- description: string | null;
- slug: string;
- image_url: string | null;
- is_active: boolean | null;
- sort_order: string | null;
- created_at: string;
- updated_at: string;
-}
-interface PaginationMeta {
- total: number;
- limit: number;
- offset: number;
- has_more: boolean;
-}
-interface PaginatedResponse<T> {
- data: T[];
- pagination: PaginationMeta;
-}
-interface EcommapsCartItem {
- id: string;
- product_id: string;
- variant_id: string | null;
- product_name: string;
- product_price: number;
- product_image: string | null;
- quantity: number;
- subtotal: number;
-}
-interface EcommapsCart {
- id: string;
- site_id: string;
- items: EcommapsCartItem[];
- items_count: number;
- subtotal: number;
- created_at: string | null;
-}
-interface EcommapsCustomer {
- id: string;
- store_id: string;
- email: string;
- full_name: string;
- phone: string | null;
- addresses: any[];
- created_at: string;
-}
-interface EcommapsAuthResponse {
- token: string;
- user: EcommapsCustomer;
-}
-interface EcommapsSearchResponse {
- data: EcommapsProduct[];
- query: string;
- pagination: PaginationMeta;
-}
-interface EcommapsAppliedDiscount {
- id?: string;
- code?: string;
- discount_type?: "percentage" | "fixed_amount" | "free_shipping";
- discount_value?: number;
- discount_amount?: number;
- promotion_type?: string;
- target_type?: string;
- message?: string;
-}
-interface EcommapsCouponValidateResponse {
- valid: boolean;
- applied_discounts?: EcommapsAppliedDiscount[];
- code?: string;
- discount_type?: "percentage" | "fixed_amount" | "free_shipping";
- discount_value?: number;
- discount_amount?: number;
- target_type?: string;
- promotion_type?: string;
- message?: string;
-}
-
-declare class EcommapsAPIError extends Error {
- status: number;
- constructor(message: string, status: number);
-}
-declare const ecommapsClient: {
- store: {
- retrieve: (options?: RequestInit) => Promise<EcommapsSite>;
- menus: {
- list: (options?: RequestInit) => Promise<EcommapsMenu[]>;
- retrieve: (handle: string, options?: RequestInit) => Promise<EcommapsMenu>;
- };
- coupons: {
- validate: (body: {
- code: string;
- cart_total?: number;
- items?: any[];
- }, options?: RequestInit) => Promise<EcommapsCouponValidateResponse>;
- };
- };
- products: {
- list: (params?: Record<string, string | number>, options?: RequestInit) => Promise<PaginatedResponse<EcommapsProduct>>;
- retrieve: (slug: string, options?: RequestInit) => Promise<EcommapsProduct>;
- search: (q: string, params?: Record<string, string | number>, options?: RequestInit) => Promise<EcommapsSearchResponse>;
- };
- collections: {
- list: (options?: RequestInit) => Promise<{
- data: EcommapsCollection[];
- }>;
- retrieve: (slug: string, limit?: number, offset?: number, options?: RequestInit) => Promise<{
- collection: EcommapsCollection;
- products: EcommapsProduct[];
- pagination: any;
- }>;
- };
- cart: {
- create: (options?: RequestInit) => Promise<EcommapsCart>;
- retrieve: (cartId: string, options?: RequestInit) => Promise<EcommapsCart>;
- addItem: (cartId: string, body: {
- product_id: string;
- variant_id?: string;
- quantity: number;
- }, options?: RequestInit) => Promise<EcommapsCart>;
- updateItem: (cartId: string, itemId: string, body: {
- quantity: number;
- }, options?: RequestInit) => Promise<EcommapsCart>;
- removeItem: (cartId: string, itemId: string, options?: RequestInit) => Promise<EcommapsCart>;
... diff truncated: showing 800 of 3276 lines
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
| }, | ||
| retrieve: (slug: string, options?: RequestInit) => ecommapsFetch<EcommapsProduct>(`/products/${slug}`, options), | ||
| search: async (q: string, params?: QueryParams, options?: RequestInit) => { | ||
| const payload = await ecommapsFetch<unknown>(buildEndpoint("/search", { q, ...(params || {}) }), options); |
There was a problem hiding this comment.
Search query parameter silently overridden by params spread
Medium Severity
In products.search, the call buildEndpoint("/search", { q, ...(params || {}) }) allows params to silently override the explicit q argument because the object spread of params comes after q. If a caller passes { q: "other" } inside params, the dedicated q function argument is discarded. The previous implementation used URLSearchParams.append which preserved the first q and added params separately, so this is a behavior regression.
6983ad5 to
400b38c
Compare
There was a problem hiding this comment.
Code Review
This pull request is a significant and well-executed refactoring that establishes a solid production baseline for the SDK, introducing a configurable client factory, improved type safety, response normalization, and modern packaging practices. However, a recurring security issue was identified where user-supplied parameters are directly interpolated into URL paths without sanitization, which could lead to path traversal vulnerabilities if the SDK is used with untrusted input. It is recommended to apply encodeURIComponent() to all path parameters. Additionally, a couple of suggestions have been provided to further enhance maintainability and correctness.
| options | ||
| ) | ||
| retrieve: async (handle: string, options?: RequestInit) => { | ||
| const payload = await ecommapsFetch<unknown>(`/store/menus/${handle}`, options); |
There was a problem hiding this comment.
The handle parameter is interpolated directly into the URL path without sanitization. If an attacker can control this value, they could perform a path traversal attack (e.g., by passing ../../another/endpoint) to access unintended API resources. Use encodeURIComponent() to sanitize the parameter.
| const payload = await ecommapsFetch<unknown>(`/store/menus/${handle}`, options); | |
| const payload = await ecommapsFetch<unknown>(`/store/menus/${encodeURIComponent(handle)}`, options); |
| return normalizeArrayPayload<EcommapsPage>(payload); | ||
| }, | ||
| retrieve: async (slug: string, options?: RequestInit) => { | ||
| const payload = await ecommapsFetch<unknown>(`/pages/${slug}`, options); |
There was a problem hiding this comment.
The slug parameter is interpolated directly into the URL path without sanitization. This could allow path traversal attacks if the input is not trusted. Use encodeURIComponent() to sanitize the parameter.
| const payload = await ecommapsFetch<unknown>(`/pages/${slug}`, options); | |
| const payload = await ecommapsFetch<unknown>(`/pages/${encodeURIComponent(slug)}`, options); |
| return normalizeArrayPayload<EcommapsBlog>(payload); | ||
| }, | ||
| retrieve: async (slug: string, options?: RequestInit) => { | ||
| const payload = await ecommapsFetch<unknown>(`/blogs/${slug}`, options); |
There was a problem hiding this comment.
The slug parameter is interpolated directly into the URL path without sanitization. This could allow path traversal attacks if the input is not trusted. Use encodeURIComponent() to sanitize the parameter.
| const payload = await ecommapsFetch<unknown>(`/blogs/${slug}`, options); | |
| const payload = await ecommapsFetch<unknown>(`/blogs/${encodeURIComponent(slug)}`, options); |
| const payload = await ecommapsFetch<unknown>(buildEndpoint("/products", params), options); | ||
| return normalizePaginatedPayload<EcommapsProduct>(payload); | ||
| }, | ||
| retrieve: (slug: string, options?: RequestInit) => ecommapsFetch<EcommapsProduct>(`/products/${slug}`, options), |
There was a problem hiding this comment.
The slug parameter is interpolated directly into the URL path without sanitization. This could allow path traversal attacks if the input is not trusted. Use encodeURIComponent() to sanitize the parameter.
| retrieve: (slug: string, options?: RequestInit) => ecommapsFetch<EcommapsProduct>(`/products/${slug}`, options), | |
| retrieve: (slug: string, options?: RequestInit) => ecommapsFetch<EcommapsProduct>(`/products/${encodeURIComponent(slug)}`, options), |
| collection: EcommapsCollection; | ||
| products: EcommapsProduct[]; | ||
| pagination: unknown; | ||
| }>(`/collections/${slug}?limit=${limit}&offset=${offset}`, options), |
There was a problem hiding this comment.
The slug parameter is interpolated directly into the URL path without sanitization. This could allow path traversal attacks if the input is not trusted. Use encodeURIComponent() to sanitize the parameter.
| }>(`/collections/${slug}?limit=${limit}&offset=${offset}`, options), | |
| }>(`/collections/${encodeURIComponent(slug)}?limit=${limit}&offset=${offset}`, options), |
| method: "POST", | ||
| body: JSON.stringify(body), | ||
| }), | ||
| retrieve: (orderNumber: string, options?: RequestInit) => ecommapsFetch<unknown>(`/orders/${orderNumber}`, options), |
There was a problem hiding this comment.
The orderNumber parameter is interpolated directly into the URL path without sanitization. This could allow path traversal attacks if the input is not trusted. Use encodeURIComponent() to sanitize the parameter.
| retrieve: (orderNumber: string, options?: RequestInit) => ecommapsFetch<unknown>(`/orders/${orderNumber}`, options), | |
| retrieve: (orderNumber: string, options?: RequestInit) => ecommapsFetch<unknown>(`/orders/${encodeURIComponent(orderNumber)}`, options), |
| body: JSON.stringify(body), | ||
| }), | ||
| setDefaultAddress: (addressId: string, options?: RequestInit) => | ||
| ecommapsFetch<{ success: boolean; addresses: unknown[] }>(`/auth/me/addresses/${addressId}/default`, { |
There was a problem hiding this comment.
The addressId parameter is interpolated directly into the URL path without sanitization. This could allow path traversal attacks if the input is not trusted. Use encodeURIComponent() to sanitize the parameter.
ecommapsFetch<<{ success: boolean; addresses: unknown[] }>(`/auth/me/addresses/${encodeURIComponent(addressId)}/default`, {| method: "PATCH", | ||
| }), | ||
| deleteAddress: (addressId: string, options?: RequestInit) => | ||
| ecommapsFetch<{ success: boolean; addresses: unknown[] }>(`/auth/me/addresses/${addressId}`, { |
There was a problem hiding this comment.
The addressId parameter is interpolated directly into the URL path without sanitization. This could allow path traversal attacks if the input is not trusted. Use encodeURIComponent() to sanitize the parameter.
ecommapsFetch<{ success: boolean; addresses: unknown[] }>(`/auth/me/addresses/${encodeURIComponent(addressId)}`, {| "clean": "rm -rf dist" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.3.0", |
There was a problem hiding this comment.
The version specified for @types/node is ^25.3.0. This appears to be a typo, as Node.js v25 does not exist. The engines field in this package.json specifies node: ">=20", so the types should align with a supported Node.js version. Using an invalid version for types can lead to incorrect type definitions and compatibility issues. I suggest correcting this to a stable version range compatible with Node 20.
| "@types/node": "^25.3.0", | |
| "@types/node": "^20.0.0", |
| option_values?: Record<string, string>; | ||
| options?: string[] | Record<string, string> | null; |
There was a problem hiding this comment.
The EcommapsProductVariant interface includes both option_values and options. This could be confusing for consumers of the SDK as they seem to serve a similar purpose. The name option_values is more descriptive than options.
To improve clarity, I recommend adding TSDoc comments to explain the purpose of each field and mark options as deprecated if option_values is the preferred property going forward.
Here is a suggested implementation:
/**
* The specific option values for this variant, e.g., `{ "Size": "Large", "Color": "Red" }`.
* This is the preferred way to access variant options.
*/
option_values?: Record<string, string>;
/**
* @deprecated Use `option_values` instead. This field is kept for backward compatibility.
*/
options?: string[] | Record<string, string> | null;


Note
Medium Risk
Refactors core client construction and response-shape handling (normalizing
[]vs{data: []}), which could subtly change runtime outputs for consumers. Also introduces new CI/release automation and packaging metadata that affects publishing behavior.Overview
SDK API update (v1.4.0): Adds
createEcommapsClient(config)(customapiUrl,apiKey,fetch, and default headers) while keeping the defaultecommapsClientsingleton.API surface + runtime behavior: Introduces typed
store.pagesandstore.blogsendpoints and tightens typings for products/variants/media, promotions/coupons, and customer addresses. Client methods now normalize common response shapes (e.g.,[]vs{ data: [] }, and record payloads underdata/result) and broaden error message extraction (detail/message).Project/release baseline: Adds GitHub Actions
CI(install/typecheck/build/pack smoke test) and a tag-drivenReleaseworkflow (npm publish --provenance), updatespackage.jsonmetadata/exports/files/engines, addspnpm-lock.yaml, and removes committeddist/andnode_modules/artifacts (now ignored).Written by Cursor Bugbot for commit 400b38c. This will update automatically on new commits. Configure here.