diff --git a/actions.js b/actions.js
new file mode 100644
index 0000000000..a990e7b913
--- /dev/null
+++ b/actions.js
@@ -0,0 +1,61 @@
+const { getTrackedImages, findTrackedImage } = require('./imageStore');
+const { getTemplateInfo, applyEdit } = require('./expressApi');
+const { uploadImageToMeta } = require('./metaUpload');
+
+function formatUnknownImageMessage(phoneNumber) {
+ const images = getTrackedImages(phoneNumber);
+ const list = images.map((image) => `- ${image.name}`).join('\n');
+ return `I couldn't find that image. Here's what I have:\n${list}`;
+}
+
+async function actionListCampaignGraphics() {
+ // TODO: fetch from campaign API
+ return 'Graphics in your current campaign:\n1. Diwali Offer Banner\n2. Summer Sale Flyer\n3. Croma Earbuds';
+}
+
+async function actionCheckAllowedEdits(phoneNumber, imageId) {
+ const image = findTrackedImage(phoneNumber, imageId);
+ if (!image) {
+ return formatUnknownImageMessage(phoneNumber);
+ }
+ const { unlockedLayers } = getTemplateInfo(image.templateId);
+ const layerList = unlockedLayers.map((layer) => `- ${layer}`).join('\n');
+ return `Edits allowed on "${image.name}":\n${layerList}`;
+}
+
+async function actionEditGraphic(phoneNumber, imageId, edits, { sendImage }) {
+ const image = findTrackedImage(phoneNumber, imageId);
+ if (!image) {
+ return formatUnknownImageMessage(phoneNumber);
+ }
+
+ const { unlockedLayers } = getTemplateInfo(image.templateId);
+ const requestedKeys = Object.keys(edits || {});
+ const disallowedKeys = requestedKeys.filter((key) => !unlockedLayers.includes(key));
+
+ if (disallowedKeys.length > 0) {
+ const allowedList = unlockedLayers.map((layer) => `- ${layer}`).join('\n');
+ return `I can't edit ${disallowedKeys.join(', ')} on "${image.name}". Allowed edits:\n${allowedList}`;
+ }
+
+ const { mergedEdits, renderedImageUrl } = applyEdit(image.templateId, image.currentEdits, edits);
+ image.currentEdits = mergedEdits;
+
+ const uploadedUrl = await uploadImageToMeta(renderedImageUrl);
+ await sendImage(phoneNumber, uploadedUrl);
+
+ const summary = Object.entries(edits).map(([key, value]) => `• ${key}: ${value}`).join('\n');
+ return `Updated "${image.name}":\n${summary}`;
+}
+
+async function actionGenerateBulkGraphics(filename) {
+ // TODO: parse CSV/Excel and call Adobe Express API per row
+ return `Bulk generation complete! Graphics created from ${filename || 'your uploaded file'}.`;
+}
+
+module.exports = {
+ actionListCampaignGraphics,
+ actionCheckAllowedEdits,
+ actionEditGraphic,
+ actionGenerateBulkGraphics,
+};
diff --git a/actions.test.js b/actions.test.js
new file mode 100644
index 0000000000..fa88280a39
--- /dev/null
+++ b/actions.test.js
@@ -0,0 +1,66 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { actionCheckAllowedEdits, actionEditGraphic } = require('./actions');
+const { getTrackedImages } = require('./imageStore');
+
+test('actionCheckAllowedEdits lists the unlocked layers for a known image', async () => {
+ const reply = await actionCheckAllowedEdits('phone-1', 'img_1');
+ assert.match(reply, /Diwali Offer Banner/);
+ assert.match(reply, /discount_text/);
+});
+
+test('actionCheckAllowedEdits lists Price, Address, Product Image, Partner Logo for the Croma earbuds image', async () => {
+ const reply = await actionCheckAllowedEdits('phone-croma', 'img_3');
+ assert.match(reply, /Croma Earbuds/);
+ assert.match(reply, /- Price/);
+ assert.match(reply, /- Address/);
+ assert.match(reply, /- Product Image/);
+ assert.match(reply, /- Partner Logo/);
+});
+
+test('actionCheckAllowedEdits reports unknown images without throwing', async () => {
+ const reply = await actionCheckAllowedEdits('phone-2', 'img_nope');
+ assert.match(reply, /couldn't find that image/);
+});
+
+test('actionEditGraphic rejects edits outside the unlocked layers and sends nothing', async () => {
+ let sendImageCalled = false;
+ const sendImage = async () => {
+ sendImageCalled = true;
+ };
+
+ const reply = await actionEditGraphic('phone-3', 'img_3', { background_color: 'red' }, { sendImage });
+
+ assert.match(reply, /can't edit background_color/);
+ assert.equal(sendImageCalled, false);
+});
+
+test('actionEditGraphic sends the fixed updated Croma earbuds image for any allowed edit', async () => {
+ const sentCalls = [];
+ const sendImage = async (to, link) => {
+ sentCalls.push({ to, link });
+ };
+
+ const reply = await actionEditGraphic('phone-5', 'img_3', { Price: '999' }, { sendImage });
+
+ assert.match(reply, /Updated "Croma Earbuds"/);
+ assert.equal(sentCalls.length, 1);
+ assert.equal(sentCalls[0].link, 'https://s7ap1.scene7.com/is/image/varun/croma1-earbuds-updated');
+});
+
+test('actionEditGraphic applies an allowed edit, sends the updated image, and remembers the edit', async () => {
+ const sentCalls = [];
+ const sendImage = async (to, link) => {
+ sentCalls.push({ to, link });
+ };
+
+ const reply = await actionEditGraphic('phone-4', 'img_2', { headline: 'Flash Sale' }, { sendImage });
+
+ assert.match(reply, /Updated "Summer Sale Flyer"/);
+ assert.equal(sentCalls.length, 1);
+ assert.equal(sentCalls[0].to, 'phone-4');
+ assert.match(sentCalls[0].link, /^https:\/\/mock-meta-cdn\.local\//);
+
+ const image = getTrackedImages('phone-4').find((img) => img.id === 'img_2');
+ assert.equal(image.currentEdits.headline, 'Flash Sale');
+});
diff --git a/app.js b/app.js
index 5ab128e4b4..1d703d50e1 100644
--- a/app.js
+++ b/app.js
@@ -1,61 +1,267 @@
-const express = require("express");
+const express = require('express');
+const OpenAI = require('openai');
+const { getTrackedImages } = require('./imageStore');
+const {
+ actionListCampaignGraphics,
+ actionCheckAllowedEdits,
+ actionEditGraphic,
+ actionGenerateBulkGraphics,
+} = require('./actions');
+
const app = express();
-const port = process.env.PORT || 3001;
-
-app.get("/", (req, res) => res.type('html').send(html));
-
-const server = app.listen(port, () => console.log(`Example app listening on port ${port}!`));
-
-server.keepAliveTimeout = 120 * 1000;
-server.headersTimeout = 120 * 1000;
-
-const html = `
-
-
-
- Hello from Render!
-
-
-
-
-
-
-
-
-`
+
+ await sendText(phoneNumber, replyText);
+ appendHistory(phoneNumber, 'assistant', replyText);
+ }
+ } catch (err) {
+ console.error('Error handling message:', {
+ message: err.message,
+ status: err.status,
+ code: err.code,
+ type: err.type,
+ requestID: err.requestID,
+ error: err.error,
+ model: openaiModel,
+ baseURL: openai.baseURL,
+ });
+ }
+});
+
+app.listen(port, () => {
+ console.log(`\nListening on port ${port}\n`);
+ console.log(`[startup] OPENAI_BASE_URL: ${process.env.OPENAI_BASE_URL ?? '(unset, defaults to api.openai.com)'}`);
+ console.log(`[startup] OPENAI_MODEL: ${openaiModel}`);
+ console.log(`[startup] OPENAI_API_KEY set: ${Boolean(process.env.OPENAI_API_KEY)}`);
+});
diff --git a/docs/superpowers/plans/2026-07-13-express-edit-flow.md b/docs/superpowers/plans/2026-07-13-express-edit-flow.md
new file mode 100644
index 0000000000..b36d079cae
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-13-express-edit-flow.md
@@ -0,0 +1,705 @@
+# Express Image Edit Flow Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Let a WhatsApp user ask what edits are allowed on a previously-sent graphic, request an edit, have it validated against the graphic's unlocked layers, applied via a mocked Adobe Express API, uploaded via a mocked Meta upload, and re-sent as an updated image — all with placeholder Express/Meta calls that can be swapped for real ones later.
+
+**Architecture:** Three new small, dependency-free modules (`imageStore.js` for per-phone-number tracked-image state, `expressApi.js` for mock template/edit calls, `metaUpload.js` for the mock media upload) get unit tests via Node's built-in test runner. A fourth new module, `actions.js`, extracts all GPT tool-call action handlers (including the two existing ones, `list_campaign_graphics` and `generate_bulk_graphics`, moved unchanged) so the new `edit_graphic` logic can be unit-tested with a fake `sendImage` — no real WhatsApp/OpenAI credentials needed for tests. `app.js` is left as the thin orchestrator: WhatsApp API calls, GPT tool schemas/system prompt, and the webhook route wiring args into `actions.js`.
+
+**Tech Stack:** Node.js 20, Express 5, `openai` SDK, Node's built-in `node:test` + `node:assert/strict` (no new dependencies).
+
+## Global Constraints
+
+- No real Adobe Express or Meta API calls — all such calls are placeholder/mock functions returning mock values (per spec, these get swapped for real implementations later).
+- Actually sending the initial graphic image is out of scope — tracked images are pre-seeded in memory at first access per phone number (spec: 3 fixed seed entries: Diwali Offer Banner / Summer Sale Flyer / New Arrival Poster).
+- State is in-memory only, no persistence across restarts (matches existing `conversationHistory` pattern in app.js).
+- No new npm dependencies — use Node's built-in `node:test` test runner.
+- Follow existing code style: CommonJS `require`, 2-space indentation, semicolons.
+
+---
+
+## Task 1: `imageStore.js` — tracked-image state
+
+**Files:**
+- Create: `imageStore.js`
+- Test: `imageStore.test.js`
+- Modify: `package.json` (add a `test` script)
+
+**Interfaces:**
+- Produces: `getTrackedImages(phoneNumber) -> Array<{ id: string, name: string, templateId: string, currentEdits: object }>` (seeds 3 fixed entries on first call for a phone number, returns the same array reference on later calls).
+- Produces: `findTrackedImage(phoneNumber, imageId) -> object | undefined`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `imageStore.test.js`:
+
+```js
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { getTrackedImages, findTrackedImage } = require('./imageStore');
+
+test('getTrackedImages seeds 3 images on first access', () => {
+ const images = getTrackedImages('111');
+ assert.equal(images.length, 3);
+ assert.deepEqual(images.map((img) => img.id), ['img_1', 'img_2', 'img_3']);
+ assert.deepEqual(images[0].currentEdits, {});
+});
+
+test('getTrackedImages returns the same array on repeated calls for the same phone number', () => {
+ const first = getTrackedImages('222');
+ first[0].currentEdits.headline = 'Flash Sale';
+ const second = getTrackedImages('222');
+ assert.equal(second[0].currentEdits.headline, 'Flash Sale');
+});
+
+test('getTrackedImages seeds independently per phone number', () => {
+ getTrackedImages('333')[0].currentEdits.headline = 'Only for 333';
+ const other = getTrackedImages('444');
+ assert.deepEqual(other[0].currentEdits, {});
+});
+
+test('findTrackedImage returns the matching image', () => {
+ getTrackedImages('555');
+ const image = findTrackedImage('555', 'img_2');
+ assert.equal(image.name, 'Summer Sale Flyer');
+});
+
+test('findTrackedImage returns undefined for an unknown id', () => {
+ getTrackedImages('666');
+ const image = findTrackedImage('666', 'img_999');
+ assert.equal(image, undefined);
+});
+```
+
+- [ ] **Step 2: Add the test script and run to verify failure**
+
+Modify `package.json` scripts section to:
+
+```json
+"scripts": {
+ "start": "node app.js",
+ "test": "node --test"
+},
+```
+
+Run: `node --test imageStore.test.js`
+Expected: FAIL — `Cannot find module './imageStore'`
+
+- [ ] **Step 3: Write the implementation**
+
+Create `imageStore.js`:
+
+```js
+const SEED_IMAGES = [
+ { id: 'img_1', name: 'Diwali Offer Banner', templateId: 'tpl_diwali' },
+ { id: 'img_2', name: 'Summer Sale Flyer', templateId: 'tpl_summer' },
+ { id: 'img_3', name: 'New Arrival Poster', templateId: 'tpl_newarrival' },
+];
+
+const trackedImages = new Map();
+
+function getTrackedImages(phoneNumber) {
+ if (!trackedImages.has(phoneNumber)) {
+ trackedImages.set(
+ phoneNumber,
+ SEED_IMAGES.map((image) => ({ ...image, currentEdits: {} }))
+ );
+ }
+ return trackedImages.get(phoneNumber);
+}
+
+function findTrackedImage(phoneNumber, imageId) {
+ return getTrackedImages(phoneNumber).find((image) => image.id === imageId);
+}
+
+module.exports = { getTrackedImages, findTrackedImage };
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `node --test imageStore.test.js`
+Expected: PASS — 5 tests passing
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add imageStore.js imageStore.test.js package.json
+git commit -m "feat: add in-memory tracked-image store"
+```
+
+---
+
+## Task 2: `expressApi.js` — mock Adobe Express calls
+
+**Files:**
+- Create: `expressApi.js`
+- Test: `expressApi.test.js`
+
+**Interfaces:**
+- Consumes: nothing (standalone module).
+- Produces: `getTemplateInfo(templateId) -> { templateId: string, unlockedLayers: string[] }`.
+- Produces: `applyEdit(templateId, currentEdits, newEdits) -> { mergedEdits: object, renderedImageUrl: string }`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `expressApi.test.js`:
+
+```js
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { getTemplateInfo, applyEdit } = require('./expressApi');
+
+test('getTemplateInfo returns the unlocked layers for a known template', () => {
+ const info = getTemplateInfo('tpl_diwali');
+ assert.deepEqual(info, {
+ templateId: 'tpl_diwali',
+ unlockedLayers: ['discount_text', 'headline', 'background_color'],
+ });
+});
+
+test('getTemplateInfo returns an empty layer list for an unknown template', () => {
+ const info = getTemplateInfo('tpl_does_not_exist');
+ assert.deepEqual(info.unlockedLayers, []);
+});
+
+test('applyEdit merges new edits on top of current edits', () => {
+ const result = applyEdit('tpl_diwali', { headline: 'Old Headline' }, { discount_text: '70%' });
+ assert.deepEqual(result.mergedEdits, { headline: 'Old Headline', discount_text: '70%' });
+});
+
+test('applyEdit returns a rendered image url that references the template', () => {
+ const result = applyEdit('tpl_summer', {}, { headline: 'Flash Sale' });
+ assert.match(result.renderedImageUrl, /^https:\/\/mock-express\.local\/render\/tpl_summer\?rev=\d+$/);
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `node --test expressApi.test.js`
+Expected: FAIL — `Cannot find module './expressApi'`
+
+- [ ] **Step 3: Write the implementation**
+
+Create `expressApi.js`:
+
+```js
+const TEMPLATE_LAYERS = {
+ tpl_diwali: ['discount_text', 'headline', 'background_color'],
+ tpl_summer: ['headline', 'font_color'],
+ tpl_newarrival: ['headline'],
+};
+
+function getTemplateInfo(templateId) {
+ return {
+ templateId,
+ unlockedLayers: TEMPLATE_LAYERS[templateId] || [],
+ };
+}
+
+let renderRevision = 0;
+
+function applyEdit(templateId, currentEdits, newEdits) {
+ const mergedEdits = { ...currentEdits, ...newEdits };
+ renderRevision += 1;
+ return {
+ mergedEdits,
+ renderedImageUrl: `https://mock-express.local/render/${templateId}?rev=${renderRevision}`,
+ };
+}
+
+module.exports = { getTemplateInfo, applyEdit };
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `node --test expressApi.test.js`
+Expected: PASS — 4 tests passing
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add expressApi.js expressApi.test.js
+git commit -m "feat: add mock Adobe Express API module"
+```
+
+---
+
+## Task 3: `metaUpload.js` — mock Meta media upload
+
+**Files:**
+- Create: `metaUpload.js`
+- Test: `metaUpload.test.js`
+
+**Interfaces:**
+- Consumes: nothing (standalone module).
+- Produces: `uploadImageToMeta(renderedImageUrl) -> Promise` (mock CDN URL).
+
+- [ ] **Step 1: Write the failing test**
+
+Create `metaUpload.test.js`:
+
+```js
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { uploadImageToMeta } = require('./metaUpload');
+
+test('uploadImageToMeta returns a mock CDN url', async () => {
+ const url = await uploadImageToMeta('https://mock-express.local/render/tpl_diwali?rev=1');
+ assert.match(url, /^https:\/\/mock-meta-cdn\.local\/media\/[0-9a-f-]+\.png$/);
+});
+
+test('uploadImageToMeta returns a different url on each call', async () => {
+ const first = await uploadImageToMeta('https://mock-express.local/render/tpl_diwali?rev=1');
+ const second = await uploadImageToMeta('https://mock-express.local/render/tpl_diwali?rev=2');
+ assert.notEqual(first, second);
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `node --test metaUpload.test.js`
+Expected: FAIL — `Cannot find module './metaUpload'`
+
+- [ ] **Step 3: Write the implementation**
+
+Create `metaUpload.js`:
+
+```js
+const { randomUUID } = require('node:crypto');
+
+async function uploadImageToMeta(renderedImageUrl) {
+ return `https://mock-meta-cdn.local/media/${randomUUID()}.png`;
+}
+
+module.exports = { uploadImageToMeta };
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `node --test metaUpload.test.js`
+Expected: PASS — 2 tests passing
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add metaUpload.js metaUpload.test.js
+git commit -m "feat: add mock Meta media upload module"
+```
+
+---
+
+## Task 4: `actions.js` — extract and extend GPT tool-call action handlers
+
+**Files:**
+- Create: `actions.js`
+- Test: `actions.test.js`
+
+**Interfaces:**
+- Consumes: `getTrackedImages`, `findTrackedImage` from `./imageStore` (Task 1); `getTemplateInfo`, `applyEdit` from `./expressApi` (Task 2); `uploadImageToMeta` from `./metaUpload` (Task 3).
+- Produces: `actionListCampaignGraphics() -> Promise` (unchanged behavior, moved from app.js).
+- Produces: `actionCheckAllowedEdits(phoneNumber, imageId) -> Promise`.
+- Produces: `actionEditGraphic(phoneNumber, imageId, edits, { sendImage }) -> Promise` — `sendImage` is injected `(to, link) => Promise` so tests don't need real WhatsApp calls.
+- Produces: `actionGenerateBulkGraphics(filename) -> Promise` (unchanged behavior, moved from app.js).
+
+- [ ] **Step 1: Write the failing test**
+
+Create `actions.test.js`:
+
+```js
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { actionCheckAllowedEdits, actionEditGraphic } = require('./actions');
+const { getTrackedImages } = require('./imageStore');
+
+test('actionCheckAllowedEdits lists the unlocked layers for a known image', async () => {
+ const reply = await actionCheckAllowedEdits('phone-1', 'img_1');
+ assert.match(reply, /Diwali Offer Banner/);
+ assert.match(reply, /discount_text/);
+});
+
+test('actionCheckAllowedEdits reports unknown images without throwing', async () => {
+ const reply = await actionCheckAllowedEdits('phone-2', 'img_nope');
+ assert.match(reply, /couldn't find that image/);
+});
+
+test('actionEditGraphic rejects edits outside the unlocked layers and sends nothing', async () => {
+ let sendImageCalled = false;
+ const sendImage = async () => {
+ sendImageCalled = true;
+ };
+
+ const reply = await actionEditGraphic('phone-3', 'img_3', { background_color: 'red' }, { sendImage });
+
+ assert.match(reply, /can't edit background_color/);
+ assert.equal(sendImageCalled, false);
+});
+
+test('actionEditGraphic applies an allowed edit, sends the updated image, and remembers the edit', async () => {
+ const sentCalls = [];
+ const sendImage = async (to, link) => {
+ sentCalls.push({ to, link });
+ };
+
+ const reply = await actionEditGraphic('phone-4', 'img_2', { headline: 'Flash Sale' }, { sendImage });
+
+ assert.match(reply, /Updated "Summer Sale Flyer"/);
+ assert.equal(sentCalls.length, 1);
+ assert.equal(sentCalls[0].to, 'phone-4');
+ assert.match(sentCalls[0].link, /^https:\/\/mock-meta-cdn\.local\//);
+
+ const image = getTrackedImages('phone-4').find((img) => img.id === 'img_2');
+ assert.equal(image.currentEdits.headline, 'Flash Sale');
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `node --test actions.test.js`
+Expected: FAIL — `Cannot find module './actions'`
+
+- [ ] **Step 3: Write the implementation**
+
+Create `actions.js`:
+
+```js
+const { getTrackedImages, findTrackedImage } = require('./imageStore');
+const { getTemplateInfo, applyEdit } = require('./expressApi');
+const { uploadImageToMeta } = require('./metaUpload');
+
+function formatUnknownImageMessage(phoneNumber) {
+ const images = getTrackedImages(phoneNumber);
+ const list = images.map((image) => `- ${image.name}`).join('\n');
+ return `I couldn't find that image. Here's what I have:\n${list}`;
+}
+
+async function actionListCampaignGraphics() {
+ // TODO: fetch from campaign API
+ return 'Graphics in your current campaign:\n1. Diwali Offer Banner\n2. Summer Sale Flyer\n3. New Arrival Poster';
+}
+
+async function actionCheckAllowedEdits(phoneNumber, imageId) {
+ const image = findTrackedImage(phoneNumber, imageId);
+ if (!image) {
+ return formatUnknownImageMessage(phoneNumber);
+ }
+ const { unlockedLayers } = getTemplateInfo(image.templateId);
+ const layerList = unlockedLayers.map((layer) => `- ${layer}`).join('\n');
+ return `Edits allowed on "${image.name}":\n${layerList}`;
+}
+
+async function actionEditGraphic(phoneNumber, imageId, edits, { sendImage }) {
+ const image = findTrackedImage(phoneNumber, imageId);
+ if (!image) {
+ return formatUnknownImageMessage(phoneNumber);
+ }
+
+ const { unlockedLayers } = getTemplateInfo(image.templateId);
+ const requestedKeys = Object.keys(edits || {});
+ const disallowedKeys = requestedKeys.filter((key) => !unlockedLayers.includes(key));
+
+ if (disallowedKeys.length > 0) {
+ const allowedList = unlockedLayers.map((layer) => `- ${layer}`).join('\n');
+ return `I can't edit ${disallowedKeys.join(', ')} on "${image.name}". Allowed edits:\n${allowedList}`;
+ }
+
+ const { mergedEdits, renderedImageUrl } = applyEdit(image.templateId, image.currentEdits, edits);
+ image.currentEdits = mergedEdits;
+
+ const uploadedUrl = await uploadImageToMeta(renderedImageUrl);
+ await sendImage(phoneNumber, uploadedUrl);
+
+ const summary = Object.entries(edits).map(([key, value]) => `• ${key}: ${value}`).join('\n');
+ return `Updated "${image.name}":\n${summary}`;
+}
+
+async function actionGenerateBulkGraphics(filename) {
+ // TODO: parse CSV/Excel and call Adobe Express API per row
+ return `Bulk generation complete! Graphics created from ${filename || 'your uploaded file'}.`;
+}
+
+module.exports = {
+ actionListCampaignGraphics,
+ actionCheckAllowedEdits,
+ actionEditGraphic,
+ actionGenerateBulkGraphics,
+};
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `node --test actions.test.js`
+Expected: PASS — 4 tests passing
+
+- [ ] **Step 5: Run the full test suite**
+
+Run: `node --test`
+Expected: PASS — all tests across `imageStore.test.js`, `expressApi.test.js`, `metaUpload.test.js`, `actions.test.js` (15 tests total)
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add actions.js actions.test.js
+git commit -m "feat: extract GPT action handlers and add image-edit validation flow"
+```
+
+---
+
+## Task 5: Wire `app.js` to the new modules
+
+**Files:**
+- Modify: `app.js:1-2` (imports)
+- Modify: `app.js:59-120` (tool definitions)
+- Modify: `app.js:122-143` (delete — action handlers now live in `actions.js`)
+- Modify: `app.js:147-174` (`decideAction` system prompt)
+- Modify: `app.js:218-245` (webhook switch statement)
+
+**Interfaces:**
+- Consumes: `getTrackedImages` from `./imageStore` (Task 1); `actionListCampaignGraphics`, `actionCheckAllowedEdits`, `actionEditGraphic`, `actionGenerateBulkGraphics` from `./actions` (Task 4).
+- Produces: nothing new — this task is pure wiring, verified manually (no unit test; this is the orchestration layer that touches the real WhatsApp/OpenAI SDKs already excluded from automated testing per the spec).
+
+- [ ] **Step 1: Update imports**
+
+At the top of `app.js`, replace:
+
+```js
+const express = require('express');
+const OpenAI = require('openai');
+```
+
+with:
+
+```js
+const express = require('express');
+const OpenAI = require('openai');
+const { getTrackedImages } = require('./imageStore');
+const {
+ actionListCampaignGraphics,
+ actionCheckAllowedEdits,
+ actionEditGraphic,
+ actionGenerateBulkGraphics,
+} = require('./actions');
+```
+
+- [ ] **Step 2: Add `image_id` to the `check_allowed_edits` and `edit_graphic` tool definitions**
+
+In the `tools` array, replace the `check_allowed_edits` entry:
+
+```js
+ {
+ type: 'function',
+ function: {
+ name: 'check_allowed_edits',
+ description: 'Check what edits are permitted on the current graphic',
+ parameters: { type: 'object', properties: {} },
+ },
+ },
+```
+
+with:
+
+```js
+ {
+ type: 'function',
+ function: {
+ name: 'check_allowed_edits',
+ description:
+ 'Check what edits are permitted on a specific graphic. Pick image_id from the "Images previously sent to this user" list in the system prompt that best matches what the user is referring to.',
+ parameters: {
+ type: 'object',
+ properties: {
+ image_id: { type: 'string', description: 'The id of the image the user is asking about, from the tracked images list' },
+ },
+ required: ['image_id'],
+ },
+ },
+ },
+```
+
+And replace the `edit_graphic` entry:
+
+```js
+ {
+ type: 'function',
+ function: {
+ name: 'edit_graphic',
+ description: 'Edit the current graphic via Adobe Express API (e.g. change discount text, colors)',
+ parameters: {
+ type: 'object',
+ properties: {
+ edits: {
+ type: 'object',
+ description: 'Key-value pairs of edits to apply, e.g. { "discount_text": "70%" }',
+ },
+ },
+ required: ['edits'],
+ },
+ },
+ },
+```
+
+with:
+
+```js
+ {
+ type: 'function',
+ function: {
+ name: 'edit_graphic',
+ description:
+ 'Edit a specific graphic via Adobe Express API (e.g. change discount text, colors). Pick image_id from the "Images previously sent to this user" list in the system prompt that best matches what the user is referring to.',
+ parameters: {
+ type: 'object',
+ properties: {
+ image_id: { type: 'string', description: 'The id of the image to edit, from the tracked images list' },
+ edits: {
+ type: 'object',
+ description: 'Key-value pairs of edits to apply, e.g. { "discount_text": "70%" }',
+ },
+ },
+ required: ['image_id', 'edits'],
+ },
+ },
+ },
+```
+
+- [ ] **Step 3: Delete the old inline action handlers**
+
+Delete this entire block (now provided by `actions.js`):
+
+```js
+// ── Action handlers (stubs — wire real APIs here) ────────────────────────────
+
+async function actionListCampaignGraphics() {
+ // TODO: fetch from campaign API
+ return 'Graphics in your current campaign:\n1. Diwali Offer Banner\n2. Summer Sale Flyer\n3. New Arrival Poster';
+}
+
+async function actionCheckAllowedEdits() {
+ // TODO: fetch from Adobe Express API
+ return 'Edits allowed on the current graphic:\n- Discount percentage\n- Headline text\n- Background color\n- Font color';
+}
+
+async function actionEditGraphic(edits) {
+ // TODO: call Adobe Express API
+ const summary = Object.entries(edits).map(([k, v]) => `• ${k}: ${v}`).join('\n');
+ return `Graphic updated successfully:\n${summary}`;
+}
+
+async function actionGenerateBulkGraphics(filename) {
+ // TODO: parse CSV/Excel and call Adobe Express API per row
+ return `Bulk generation complete! Graphics created from ${filename || 'your uploaded file'}.`;
+}
+```
+
+- [ ] **Step 4: Add the tracked-images list to the `decideAction` system prompt**
+
+Replace:
+
+```js
+async function decideAction(phoneNumber, userMessage) {
+ const last3 = getHistory(phoneNumber).slice(-3);
+
+ const messages = [
+ {
+ role: 'system',
+ content: `You are a WhatsApp assistant for managing marketing campaign graphics via Adobe Express.
+Analyze the user's message and conversation history, then call the appropriate tool.
+Always call exactly one tool — never reply with plain text.
+If the request is ambiguous or missing details, use ask_for_more_information.`,
+ },
+ ...last3,
+ { role: 'user', content: userMessage },
+ ];
+```
+
+with:
+
+```js
+async function decideAction(phoneNumber, userMessage) {
+ const last3 = getHistory(phoneNumber).slice(-3);
+ const trackedImages = getTrackedImages(phoneNumber);
+ const imagesList = trackedImages.map((image) => `- ${image.id}: ${image.name}`).join('\n');
+
+ const messages = [
+ {
+ role: 'system',
+ content: `You are a WhatsApp assistant for managing marketing campaign graphics via Adobe Express.
+Analyze the user's message and conversation history, then call the appropriate tool.
+Always call exactly one tool — never reply with plain text.
+If the request is ambiguous or missing details, use ask_for_more_information.
+
+Images previously sent to this user (reference by id):
+${imagesList}`,
+ },
+ ...last3,
+ { role: 'user', content: userMessage },
+ ];
+```
+
+- [ ] **Step 5: Pass `phoneNumber`/`image_id`/`sendImage` through the webhook switch**
+
+Replace:
+
+```js
+ case 'check_allowed_edits':
+ await sendText(phoneNumber, '⏳ Checking allowed edits...');
+ replyText = await actionCheckAllowedEdits();
+ break;
+
+ case 'edit_graphic':
+ await sendText(phoneNumber, '⏳ Applying edits to your graphic...');
+ replyText = await actionEditGraphic(args.edits);
+ break;
+```
+
+with:
+
+```js
+ case 'check_allowed_edits':
+ await sendText(phoneNumber, '⏳ Checking allowed edits...');
+ replyText = await actionCheckAllowedEdits(phoneNumber, args.image_id);
+ break;
+
+ case 'edit_graphic':
+ await sendText(phoneNumber, '⏳ Applying edits to your graphic...');
+ replyText = await actionEditGraphic(phoneNumber, args.image_id, args.edits, { sendImage });
+ break;
+```
+
+- [ ] **Step 6: Verify the file is syntactically valid**
+
+Run: `node --check app.js`
+Expected: no output (silent success)
+
+- [ ] **Step 7: Verify the server still boots and the webhook-verification route still works**
+
+Run:
+
+```bash
+VERIFY_TOKEN=test WHATSAPP_PHONE_NUMBER_ID=123 WHATSAPP_TOKEN=test OPENAI_API_KEY=test node app.js &
+SERVER_PID=$!
+sleep 1
+curl -s "http://localhost:3000/?hub.mode=subscribe&hub.verify_token=test&hub.challenge=hello123"
+kill $SERVER_PID
+```
+
+Expected: `hello123` printed by curl, followed by `WEBHOOK VERIFIED` in the server's stdout before it's killed.
+
+- [ ] **Step 8: Run the full test suite once more to confirm nothing broke**
+
+Run: `node --test`
+Expected: PASS — all 15 tests passing
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add app.js
+git commit -m "feat: wire image-edit flow into the webhook and GPT tool schema"
+```
+
+---
+
+## Self-Review Notes
+
+- **Spec coverage:** template/layer lookup (Task 2), allowed-edit validation (Task 4 `actionEditGraphic`), Express edit + Meta upload + re-send (Task 4 + Task 5 wiring), image reference inferred by GPT from a system-prompt-provided list (Task 5 Step 4), pre-seeded mock images since real sending is out of scope (Task 1) — all covered.
+- **Placeholder scan:** no TBDs; the two `// TODO: fetch from campaign API` / `// TODO: parse CSV/Excel...` comments are carried over unchanged from the existing code (out of scope for this feature) and are intentional, not gaps in this plan.
+- **Type consistency:** `sendImage` signature `(to, link) => Promise` is consistent between `actions.js`'s `actionEditGraphic` (Task 4) and its call site in `app.js` (Task 5 Step 5), which passes the existing `sendImage(to, link)` from `app.js`. `image.id`/`image.name`/`image.templateId`/`image.currentEdits` field names are consistent across `imageStore.js`, `expressApi.js` consumers, and `actions.js`.
diff --git a/docs/superpowers/specs/2026-07-13-express-edit-flow-design.md b/docs/superpowers/specs/2026-07-13-express-edit-flow-design.md
new file mode 100644
index 0000000000..272d63626f
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-13-express-edit-flow-design.md
@@ -0,0 +1,132 @@
+# Design: Image edit flow (Adobe Express, mocked)
+
+## Goal
+
+Support this WhatsApp conversation flow:
+
+1. User has previously been sent a message containing an image (graphic).
+2. User asks what edits are allowed on one of those images.
+3. System looks up which template the image used and which layers are unlocked (via Adobe Express API).
+4. User asks for a specific edit.
+5. System checks whether the requested edit is one of the allowed edits.
+6. System calls the Adobe Express API to apply the edit.
+7. System uploads the resulting image to Meta and gets back a URL.
+8. System sends a new WhatsApp message with the updated image.
+
+Adobe Express API calls are out of scope for this iteration — all Express/Meta-upload calls are placeholder functions returning mock values, structured so they can be swapped for real implementations later without changing the surrounding flow.
+
+Actually sending the *initial* graphic image (step 1) is out of scope. Instead, a small set of mock "sent images" is pre-seeded at server startup for any phone number, so the rest of the flow has something to operate against.
+
+## Data model
+
+New in-memory store, `imageStore.js`, following the same per-phone-number Map pattern as the existing `conversationHistory`:
+
+```js
+// Map>
+// Seeded lazily on first access for a phone number — same 3 mock entries every time.
+{
+ id: 'img_1', // stable id GPT references in tool calls
+ name: 'Diwali Offer Banner', // human-readable, shown to GPT so it can match user text
+ templateId: 'tpl_diwali', // Express template id
+ currentEdits: {}, // accumulates applied edits across calls
+}
+```
+
+Three seed entries (mirroring the existing `list_campaign_graphics` mock names): Diwali Offer Banner / Summer Sale Flyer / New Arrival Poster, each with a distinct mock `templateId`.
+
+`getTrackedImages(phoneNumber)` returns (seeding if needed) the array; `findTrackedImage(phoneNumber, imageId)` returns one entry or `undefined`.
+
+## GPT tool changes
+
+GPT is responsible for figuring out *which* tracked image a user's message refers to (per your choice of "GPT infers from text"), so the system prompt must include the phone number's tracked images (id + name) on every call to `decideAction`. Both relevant tools gain an `image_id` parameter:
+
+```js
+check_allowed_edits({ image_id })
+edit_graphic({ image_id, edits })
+```
+
+`image_id` is required on both. Tool descriptions instruct GPT to pick the id from the list provided in the system prompt, matching the user's description (e.g. "the Diwali one") to the closest tracked image name.
+
+System prompt gains a section like:
+
+```
+Images previously sent to this user (reference by id):
+- img_1: Diwali Offer Banner
+- img_2: Summer Sale Flyer
+- img_3: New Arrival Poster
+```
+
+## Mock Adobe Express API (`expressApi.js`)
+
+Placeholder module, no network calls:
+
+```js
+function getTemplateInfo(templateId) {
+ // Mock: returns different unlocked layers per template so behavior isn't uniform.
+ // e.g. tpl_diwali -> ['discount_text', 'headline', 'background_color']
+ // tpl_summer -> ['headline', 'font_color']
+ // tpl_newarrival -> ['headline']
+}
+
+function applyEdit(templateId, currentEdits, newEdits) {
+ // Mock: merges edits, returns a fake rendered-image reference
+ // e.g. { renderedImageUrl: 'https://mock-express.local/render/tpl_diwali?rev=3' }
+}
+```
+
+Both are synchronous or trivially async (`Promise.resolve`) mocks — no real HTTP calls yet.
+
+## Mock Meta upload
+
+Added alongside the existing WhatsApp helpers in app.js (or a small `whatsapp.js` if we split further — not required for this scope):
+
+```js
+async function uploadImageToMeta(renderedImageUrl) {
+ // Mock: pretend to upload to Meta's media endpoint, return a fake CDN url
+ // e.g. `https://mock-meta-cdn.local/media/.png`
+}
+```
+
+`sendImage(to, link)` already exists and takes a link — the mock URL from `uploadImageToMeta` is passed straight into it.
+
+## Action handlers
+
+### `actionCheckAllowedEdits(phoneNumber, imageId)`
+
+1. `findTrackedImage(phoneNumber, imageId)` — if not found, return an error message ("I couldn't find that image — here's what I have: ...").
+2. `getTemplateInfo(image.templateId)` → `unlockedLayers`.
+3. Return a formatted list of allowed edits for that specific image (not the generic hardcoded string currently returned).
+
+### `actionEditGraphic(phoneNumber, imageId, edits)`
+
+1. `findTrackedImage(phoneNumber, imageId)` — same not-found handling as above.
+2. `getTemplateInfo(image.templateId)` → `unlockedLayers`.
+3. Validate: every key in `edits` must be in `unlockedLayers`. If any key isn't allowed, return a message naming the rejected field(s) and listing the actually-allowed fields — **do not call Express or Meta**. This is the "system checks if this is one of the allowed edits" step, done in code, not by GPT.
+4. `applyEdit(templateId, image.currentEdits, edits)` → mock render; merge `edits` into `image.currentEdits`.
+5. `uploadImageToMeta(renderedImageUrl)` → mock URL.
+6. `sendImage(phoneNumber, mockUrl)` — new WhatsApp message with the updated image.
+7. Return a short confirming text (e.g. "Updated: discount_text → 70%") which is sent as a follow-up text message via the existing `sendText` call in the webhook handler.
+
+## Webhook wiring
+
+In `app.post('/')`, the `check_allowed_edits` and `edit_graphic` cases pass `args.image_id` (and `args.edits`) through to the updated action handlers. No change to the outer loop structure.
+
+## Error handling
+
+- Unknown `image_id` (GPT hallucination): friendly text response listing currently tracked images, no crash.
+- Disallowed edit field(s): friendly text response naming what's not allowed and what is, no Express/Meta calls made.
+- Mock functions never throw — real error handling (network failures, Express API errors) is deferred to when real API calls replace the mocks.
+
+## Testing
+
+Manual verification only for this mocked iteration (no live Express/Meta credentials exist yet):
+- Simulate incoming webhook payloads for: "what edits can I make to the summer flyer", followed by "change the headline to Flash Sale" — confirm the reply lists the right mock unlocked layers and then confirms the edit + attempts a mock image send.
+- Simulate an edit request for a field not in `unlockedLayers` — confirm it's rejected with the allowed-fields message and no image is sent.
+- Simulate a reference to a nonexistent image — confirm the not-found message.
+
+## Out of scope
+
+- Real Adobe Express API integration.
+- Real Meta media upload.
+- Actually sending the initial graphic image that seeds the flow (images are pre-seeded in memory instead).
+- Persistence across server restarts (in-memory only, matches existing `conversationHistory` pattern).
diff --git a/expressApi.js b/expressApi.js
new file mode 100644
index 0000000000..0a352c054d
--- /dev/null
+++ b/expressApi.js
@@ -0,0 +1,33 @@
+const TEMPLATE_LAYERS = {
+ tpl_diwali: ['discount_text', 'headline', 'background_color'],
+ tpl_summer: ['headline', 'font_color'],
+ tpl_croma_earbuds: ['Price', 'Address', 'Product Image', 'Partner Logo'],
+};
+
+function getTemplateInfo(templateId) {
+ return {
+ templateId,
+ unlockedLayers: TEMPLATE_LAYERS[templateId] || [],
+ };
+}
+
+let renderRevision = 0;
+
+function applyEdit(templateId, currentEdits, newEdits) {
+ const mergedEdits = { ...currentEdits, ...newEdits };
+
+ if (templateId === 'tpl_croma_earbuds') {
+ return {
+ mergedEdits,
+ renderedImageUrl: 'https://s7ap1.scene7.com/is/image/varun/croma1-earbuds-updated',
+ };
+ }
+
+ renderRevision += 1;
+ return {
+ mergedEdits,
+ renderedImageUrl: `https://mock-express.local/render/${templateId}?rev=${renderRevision}`,
+ };
+}
+
+module.exports = { getTemplateInfo, applyEdit };
diff --git a/expressApi.test.js b/expressApi.test.js
new file mode 100644
index 0000000000..c9b3126535
--- /dev/null
+++ b/expressApi.test.js
@@ -0,0 +1,39 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { getTemplateInfo, applyEdit } = require('./expressApi');
+
+test('getTemplateInfo returns the unlocked layers for a known template', () => {
+ const info = getTemplateInfo('tpl_diwali');
+ assert.deepEqual(info, {
+ templateId: 'tpl_diwali',
+ unlockedLayers: ['discount_text', 'headline', 'background_color'],
+ });
+});
+
+test('getTemplateInfo returns the unlocked layers for the Croma earbuds template', () => {
+ const info = getTemplateInfo('tpl_croma_earbuds');
+ assert.deepEqual(info, {
+ templateId: 'tpl_croma_earbuds',
+ unlockedLayers: ['Price', 'Address', 'Product Image', 'Partner Logo'],
+ });
+});
+
+test('getTemplateInfo returns an empty layer list for an unknown template', () => {
+ const info = getTemplateInfo('tpl_does_not_exist');
+ assert.deepEqual(info.unlockedLayers, []);
+});
+
+test('applyEdit merges new edits on top of current edits', () => {
+ const result = applyEdit('tpl_diwali', { headline: 'Old Headline' }, { discount_text: '70%' });
+ assert.deepEqual(result.mergedEdits, { headline: 'Old Headline', discount_text: '70%' });
+});
+
+test('applyEdit returns a rendered image url that references the template', () => {
+ const result = applyEdit('tpl_summer', {}, { headline: 'Flash Sale' });
+ assert.match(result.renderedImageUrl, /^https:\/\/mock-express\.local\/render\/tpl_summer\?rev=\d+$/);
+});
+
+test('applyEdit returns the fixed updated Croma earbuds image for any edit', () => {
+ const result = applyEdit('tpl_croma_earbuds', {}, { Price: '999' });
+ assert.equal(result.renderedImageUrl, 'https://s7ap1.scene7.com/is/image/varun/croma1-earbuds-updated');
+});
diff --git a/imageStore.js b/imageStore.js
new file mode 100644
index 0000000000..136d365bc8
--- /dev/null
+++ b/imageStore.js
@@ -0,0 +1,28 @@
+const SEED_IMAGES = [
+ { id: 'img_1', name: 'Diwali Offer Banner', templateId: 'tpl_diwali' },
+ { id: 'img_2', name: 'Summer Sale Flyer', templateId: 'tpl_summer' },
+ {
+ id: 'img_3',
+ name: 'Croma Earbuds',
+ templateId: 'tpl_croma_earbuds',
+ url: 'https://s7ap1.scene7.com/is/image/varun/croma1-earbuds',
+ },
+];
+
+const trackedImages = new Map();
+
+function getTrackedImages(phoneNumber) {
+ if (!trackedImages.has(phoneNumber)) {
+ trackedImages.set(
+ phoneNumber,
+ SEED_IMAGES.map((image) => ({ ...image, currentEdits: {} }))
+ );
+ }
+ return trackedImages.get(phoneNumber);
+}
+
+function findTrackedImage(phoneNumber, imageId) {
+ return getTrackedImages(phoneNumber).find((image) => image.id === imageId);
+}
+
+module.exports = { getTrackedImages, findTrackedImage };
diff --git a/imageStore.test.js b/imageStore.test.js
new file mode 100644
index 0000000000..841acf9d78
--- /dev/null
+++ b/imageStore.test.js
@@ -0,0 +1,35 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { getTrackedImages, findTrackedImage } = require('./imageStore');
+
+test('getTrackedImages seeds 3 images on first access', () => {
+ const images = getTrackedImages('111');
+ assert.equal(images.length, 3);
+ assert.deepEqual(images.map((img) => img.id), ['img_1', 'img_2', 'img_3']);
+ assert.deepEqual(images[0].currentEdits, {});
+});
+
+test('getTrackedImages returns the same array on repeated calls for the same phone number', () => {
+ const first = getTrackedImages('222');
+ first[0].currentEdits.headline = 'Flash Sale';
+ const second = getTrackedImages('222');
+ assert.equal(second[0].currentEdits.headline, 'Flash Sale');
+});
+
+test('getTrackedImages seeds independently per phone number', () => {
+ getTrackedImages('333')[0].currentEdits.headline = 'Only for 333';
+ const other = getTrackedImages('444');
+ assert.deepEqual(other[0].currentEdits, {});
+});
+
+test('findTrackedImage returns the matching image', () => {
+ getTrackedImages('555');
+ const image = findTrackedImage('555', 'img_2');
+ assert.equal(image.name, 'Summer Sale Flyer');
+});
+
+test('findTrackedImage returns undefined for an unknown id', () => {
+ getTrackedImages('666');
+ const image = findTrackedImage('666', 'img_999');
+ assert.equal(image, undefined);
+});
diff --git a/metaUpload.js b/metaUpload.js
new file mode 100644
index 0000000000..4a6a68402b
--- /dev/null
+++ b/metaUpload.js
@@ -0,0 +1,10 @@
+const { randomUUID } = require('node:crypto');
+
+async function uploadImageToMeta(renderedImageUrl) {
+ if (!renderedImageUrl.startsWith('https://mock-express.local/')) {
+ return renderedImageUrl;
+ }
+ return `https://mock-meta-cdn.local/media/${randomUUID()}.png`;
+}
+
+module.exports = { uploadImageToMeta };
diff --git a/metaUpload.test.js b/metaUpload.test.js
new file mode 100644
index 0000000000..e4aed1b9ca
--- /dev/null
+++ b/metaUpload.test.js
@@ -0,0 +1,19 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { uploadImageToMeta } = require('./metaUpload');
+
+test('uploadImageToMeta returns a mock CDN url', async () => {
+ const url = await uploadImageToMeta('https://mock-express.local/render/tpl_diwali?rev=1');
+ assert.match(url, /^https:\/\/mock-meta-cdn\.local\/media\/[0-9a-f-]+\.png$/);
+});
+
+test('uploadImageToMeta returns a different url on each call', async () => {
+ const first = await uploadImageToMeta('https://mock-express.local/render/tpl_diwali?rev=1');
+ const second = await uploadImageToMeta('https://mock-express.local/render/tpl_diwali?rev=2');
+ assert.notEqual(first, second);
+});
+
+test('uploadImageToMeta passes through urls that are already publicly hosted', async () => {
+ const url = await uploadImageToMeta('https://s7ap1.scene7.com/is/image/varun/croma1-earbuds-updated');
+ assert.equal(url, 'https://s7ap1.scene7.com/is/image/varun/croma1-earbuds-updated');
+});
diff --git a/package.json b/package.json
index 43954fe280..854ad81ab5 100644
--- a/package.json
+++ b/package.json
@@ -8,9 +8,11 @@
"license": "MIT",
"private": false,
"scripts": {
- "start": "node app.js"
+ "start": "node app.js",
+ "test": "node --test"
},
"dependencies": {
- "express": "^5.0.0"
+ "express": "^5.0.0",
+ "openai": "^6.45.0"
}
}
diff --git a/render.yaml b/render.yaml
index 9583d53df2..55fe1154ed 100644
--- a/render.yaml
+++ b/render.yaml
@@ -8,3 +8,15 @@ services:
envVars:
- key: NODE_ENV
value: production
+ - key: VERIFY_TOKEN
+ sync: false
+ - key: WHATSAPP_PHONE_NUMBER_ID
+ sync: false
+ - key: WHATSAPP_TOKEN
+ sync: false
+ - key: OPENAI_API_KEY
+ sync: false
+ - key: OPENAI_BASE_URL
+ sync: false
+ - key: OPENAI_MODEL
+ sync: false
diff --git a/yarn.lock b/yarn.lock
index 35c2bede4d..e4212e3db7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4,570 +4,486 @@
accepts@^2.0.0:
version "2.0.0"
- resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
+ resolved "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==
dependencies:
mime-types "^3.0.0"
negotiator "^1.0.0"
-array-flatten@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-3.0.0.tgz#6428ca2ee52c7b823192ec600fa3ed2f157cd541"
- integrity sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==
-
-body-parser@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.0.1.tgz#979de4a43468c5624403457fd6d45f797faffbaf"
- integrity sha512-PagxbjvuPH6tv0f/kdVbFGcb79D236SLcDTs6DrQ7GizJ88S1UWP4nMXFEo/I4fdhGRGabvFfFjVGm3M7U8JwA==
+body-parser@^2.2.1:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437"
+ integrity sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==
dependencies:
- bytes "3.1.2"
- content-type "~1.0.5"
- debug "3.1.0"
- destroy "1.2.0"
- http-errors "2.0.0"
- iconv-lite "0.5.2"
- on-finished "2.4.1"
- qs "6.13.0"
- raw-body "^3.0.0"
- type-is "~1.6.18"
- unpipe "1.0.0"
-
-bytes@3.1.2:
+ bytes "^3.1.2"
+ content-type "^2.0.0"
+ debug "^4.4.3"
+ http-errors "^2.0.1"
+ iconv-lite "^0.7.2"
+ on-finished "^2.4.1"
+ qs "^6.15.2"
+ raw-body "^3.0.2"
+ type-is "^2.1.0"
+
+bytes@^3.1.2, bytes@~3.1.2:
version "3.1.2"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+ resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
-call-bind@^1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
- integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
- es-define-property "^1.0.0"
es-errors "^1.3.0"
function-bind "^1.1.2"
- get-intrinsic "^1.2.4"
- set-function-length "^1.2.1"
-content-disposition@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2"
- integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==
+call-bound@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+ integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
dependencies:
- safe-buffer "5.2.1"
+ call-bind-apply-helpers "^1.0.2"
+ get-intrinsic "^1.3.0"
-content-type@^1.0.5, content-type@~1.0.5:
+content-disposition@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz#f3db789c752d45564cc7e9e1e0b31790d4a38e17"
+ integrity sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==
+
+content-type@^1.0.5:
version "1.0.5"
- resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
+ resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
-content-type@~1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
- integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+content-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz#2fb3ede69dffa0af78ca7c4ce7589680638b56df"
+ integrity sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==
cookie-signature@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.1.tgz#790dea2cce64638c7ae04d9fabed193bd7ccf3b4"
- integrity sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==
-
-cookie@0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
- integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
-
-debug@2.6.9:
- version "2.6.9"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
- integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
- dependencies:
- ms "2.0.0"
-
-debug@3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
- integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
- dependencies:
- ms "2.0.0"
+ version "1.2.2"
+ resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793"
+ integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==
-debug@4.3.6:
- version "4.3.6"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
- integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
- dependencies:
- ms "2.1.2"
+cookie@^0.7.1:
+ version "0.7.2"
+ resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
-debug@^4.3.5:
- version "4.3.7"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
- integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+debug@^4.4.0, debug@^4.4.3:
+ version "4.4.3"
+ resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
+ integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
-define-data-property@^1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
- integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
- dependencies:
- es-define-property "^1.0.0"
- es-errors "^1.3.0"
- gopd "^1.0.1"
-
-depd@2.0.0:
+depd@^2.0.0, depd@~2.0.0:
version "2.0.0"
- resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+ resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
-destroy@1.2.0, destroy@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
- integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
ee-first@1.1.1:
version "1.1.1"
- resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
- integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+ resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
-encodeurl@^2.0.0, encodeurl@~2.0.0:
+encodeurl@^2.0.0:
version "2.0.0"
- resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
+ resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
-encodeurl@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
- integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
-
-es-define-property@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
- integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
- dependencies:
- get-intrinsic "^1.2.4"
+es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
- resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
-escape-html@^1.0.3, escape-html@~1.0.3:
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b"
+ integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==
+ dependencies:
+ es-errors "^1.3.0"
+
+escape-html@^1.0.3:
version "1.0.3"
- resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
- integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+ resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
-etag@^1.8.1, etag@~1.8.1:
+etag@^1.8.1:
version "1.8.1"
- resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
- integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+ resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
express@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/express/-/express-5.0.0.tgz#744f9ec86025a01aeca99e4300aa4fc050d493c7"
- integrity sha512-V4UkHQc+B7ldh1YC84HCXHwf60M4BOMvp9rkvTUWCK5apqDC1Esnbid4wm6nFyVuDy8XMfETsJw5lsIGBWyo0A==
+ version "5.2.1"
+ resolved "https://registry.npmjs.org/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04"
+ integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==
dependencies:
accepts "^2.0.0"
- body-parser "^2.0.1"
+ body-parser "^2.2.1"
content-disposition "^1.0.0"
- content-type "~1.0.4"
- cookie "0.6.0"
+ content-type "^1.0.5"
+ cookie "^0.7.1"
cookie-signature "^1.2.1"
- debug "4.3.6"
- depd "2.0.0"
- encodeurl "~2.0.0"
- escape-html "~1.0.3"
- etag "~1.8.1"
- finalhandler "^2.0.0"
- fresh "2.0.0"
- http-errors "2.0.0"
+ debug "^4.4.0"
+ depd "^2.0.0"
+ encodeurl "^2.0.0"
+ escape-html "^1.0.3"
+ etag "^1.8.1"
+ finalhandler "^2.1.0"
+ fresh "^2.0.0"
+ http-errors "^2.0.0"
merge-descriptors "^2.0.0"
- methods "~1.1.2"
mime-types "^3.0.0"
- on-finished "2.4.1"
- once "1.4.0"
- parseurl "~1.3.3"
- proxy-addr "~2.0.7"
- qs "6.13.0"
- range-parser "~1.2.1"
- router "^2.0.0"
- safe-buffer "5.2.1"
+ on-finished "^2.4.1"
+ once "^1.4.0"
+ parseurl "^1.3.3"
+ proxy-addr "^2.0.7"
+ qs "^6.14.0"
+ range-parser "^1.2.1"
+ router "^2.2.0"
send "^1.1.0"
- serve-static "^2.1.0"
- setprototypeof "1.2.0"
- statuses "2.0.1"
- type-is "^2.0.0"
- utils-merge "1.0.1"
- vary "~1.1.2"
-
-finalhandler@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.0.0.tgz#9d3c79156dfa798069db7de7dd53bc37546f564b"
- integrity sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==
+ serve-static "^2.2.0"
+ statuses "^2.0.1"
+ type-is "^2.0.1"
+ vary "^1.1.2"
+
+finalhandler@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099"
+ integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==
dependencies:
- debug "2.6.9"
- encodeurl "~1.0.2"
- escape-html "~1.0.3"
- on-finished "2.4.1"
- parseurl "~1.3.3"
- statuses "2.0.1"
- unpipe "~1.0.0"
+ debug "^4.4.0"
+ encodeurl "^2.0.0"
+ escape-html "^1.0.3"
+ on-finished "^2.4.1"
+ parseurl "^1.3.3"
+ statuses "^2.0.1"
forwarded@0.2.0:
version "0.2.0"
- resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+ resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
-fresh@2.0.0:
+fresh@^2.0.0:
version "2.0.0"
- resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4"
+ resolved "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4"
integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==
-fresh@^0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
- integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
-
function-bind@^1.1.2:
version "1.1.2"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
-get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
- integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
+get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ es-define-property "^1.0.1"
es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
function-bind "^1.1.2"
- has-proto "^1.0.1"
- has-symbols "^1.0.3"
- hasown "^2.0.0"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
-gopd@^1.0.1:
+get-proto@^1.0.1:
version "1.0.1"
- resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
- integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
- dependencies:
- get-intrinsic "^1.1.3"
-
-has-property-descriptors@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
- integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+ resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
- es-define-property "^1.0.0"
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
-has-proto@^1.0.1:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
- integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
+gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
-has-symbols@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
- integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
-hasown@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
- integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+hasown@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003"
+ integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==
dependencies:
function-bind "^1.1.2"
-http-errors@2.0.0, http-errors@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
- integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
- dependencies:
- depd "2.0.0"
- inherits "2.0.4"
- setprototypeof "1.2.0"
- statuses "2.0.1"
- toidentifier "1.0.1"
-
-iconv-lite@0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.2.tgz#af6d628dccfb463b7364d97f715e4b74b8c8c2b8"
- integrity sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==
+http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
+ integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
dependencies:
- safer-buffer ">= 2.1.2 < 3"
+ depd "~2.0.0"
+ inherits "~2.0.4"
+ setprototypeof "~1.2.0"
+ statuses "~2.0.2"
+ toidentifier "~1.0.1"
-iconv-lite@0.6.3:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
- integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+iconv-lite@^0.7.2, iconv-lite@~0.7.0:
+ version "0.7.3"
+ resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz#84ee12f963e7de50bc01a13e160a078b3b0f415f"
+ integrity sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
-inherits@2.0.4:
+inherits@~2.0.4:
version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
- resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+ resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
-is-promise@4.0.0:
+is-promise@^4.0.0:
version "4.0.0"
- resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
+ resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==
-media-typer@0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
- integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
media-typer@^1.1.0:
version "1.1.0"
- resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561"
+ resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561"
integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==
merge-descriptors@^2.0.0:
version "2.0.0"
- resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808"
+ resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808"
integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==
-methods@~1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
- integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
-
-mime-db@1.40.0:
- version "1.40.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
- integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
-
-mime-db@1.52.0:
- version "1.52.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
- integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
-
-mime-db@^1.53.0:
- version "1.53.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447"
- integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==
-
-mime-types@^2.1.35:
- version "2.1.35"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
- integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
- dependencies:
- mime-db "1.52.0"
-
-mime-types@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.0.tgz#148453a900475522d095a445355c074cca4f5217"
- integrity sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==
- dependencies:
- mime-db "^1.53.0"
+mime-db@^1.54.0:
+ version "1.54.0"
+ resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
+ integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
-mime-types@~2.1.24:
- version "2.1.24"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
- integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
+mime-types@^3.0.0, mime-types@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
+ integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==
dependencies:
- mime-db "1.40.0"
-
-ms@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
- integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
-
-ms@2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
- integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+ mime-db "^1.54.0"
ms@^2.1.3:
version "2.1.3"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
negotiator@^1.0.0:
version "1.0.0"
- resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
+ resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
-object-inspect@^1.13.1:
- version "1.13.2"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
- integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
+object-inspect@^1.13.3, object-inspect@^1.13.4:
+ version "1.13.4"
+ resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
+ integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
-on-finished@2.4.1, on-finished@^2.4.1:
+on-finished@^2.4.1:
version "2.4.1"
- resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
+ resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
-once@1.4.0:
+once@^1.4.0:
version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
-parseurl@^1.3.3, parseurl@~1.3.3:
+openai@^6.45.0:
+ version "6.45.0"
+ resolved "https://registry.npmjs.org/openai/-/openai-6.45.0.tgz#53840c5c5848884dfbff47006f839b27d1d955b9"
+ integrity sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==
+
+parseurl@^1.3.3:
version "1.3.3"
- resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+ resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@^8.0.0:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.1.0.tgz#4d687606ed0be8ed512ba802eb94d620cb1a86f0"
- integrity sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==
+ version "8.4.2"
+ resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz#795c420c4f7ca45c5b887366f622ee0c9852cccd"
+ integrity sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==
-proxy-addr@~2.0.7:
+proxy-addr@^2.0.7:
version "2.0.7"
- resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+ resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
-qs@6.13.0:
- version "6.13.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
- integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
+qs@^6.14.0, qs@^6.15.2:
+ version "6.15.3"
+ resolved "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b"
+ integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==
dependencies:
- side-channel "^1.0.6"
+ es-define-property "^1.0.1"
+ side-channel "^1.1.1"
-range-parser@^1.2.1, range-parser@~1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
- integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+range-parser@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz#d7f19be812bb62721472b45d3be219ef09572b47"
+ integrity sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==
-raw-body@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f"
- integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==
+raw-body@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51"
+ integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==
dependencies:
- bytes "3.1.2"
- http-errors "2.0.0"
- iconv-lite "0.6.3"
- unpipe "1.0.0"
+ bytes "~3.1.2"
+ http-errors "~2.0.1"
+ iconv-lite "~0.7.0"
+ unpipe "~1.0.0"
-router@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/router/-/router-2.0.0.tgz#8692720b95de83876870d7bc638dd3c7e1ae8a27"
- integrity sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==
+router@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef"
+ integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==
dependencies:
- array-flatten "3.0.0"
- is-promise "4.0.0"
- methods "~1.1.2"
- parseurl "~1.3.3"
+ debug "^4.4.0"
+ depd "^2.0.0"
+ is-promise "^4.0.0"
+ parseurl "^1.3.3"
path-to-regexp "^8.0.0"
- setprototypeof "1.2.0"
- utils-merge "1.0.1"
-safe-buffer@5.2.1:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
- integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
+"safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
- resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-send@^1.0.0, send@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/send/-/send-1.1.0.tgz#4efe6ff3bb2139b0e5b2648d8b18d4dec48fc9c5"
- integrity sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==
+send@^1.1.0, send@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed"
+ integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==
dependencies:
- debug "^4.3.5"
- destroy "^1.2.0"
+ debug "^4.4.3"
encodeurl "^2.0.0"
escape-html "^1.0.3"
etag "^1.8.1"
- fresh "^0.5.2"
- http-errors "^2.0.0"
- mime-types "^2.1.35"
+ fresh "^2.0.0"
+ http-errors "^2.0.1"
+ mime-types "^3.0.2"
ms "^2.1.3"
on-finished "^2.4.1"
range-parser "^1.2.1"
- statuses "^2.0.1"
+ statuses "^2.0.2"
-serve-static@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.1.0.tgz#1b4eacbe93006b79054faa4d6d0a501d7f0e84e2"
- integrity sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==
+serve-static@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9"
+ integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==
dependencies:
encodeurl "^2.0.0"
escape-html "^1.0.3"
parseurl "^1.3.3"
- send "^1.0.0"
+ send "^1.2.0"
-set-function-length@^1.2.1:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
- integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+setprototypeof@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+ integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+side-channel-list@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127"
+ integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==
dependencies:
- define-data-property "^1.1.4"
es-errors "^1.3.0"
- function-bind "^1.1.2"
- get-intrinsic "^1.2.4"
- gopd "^1.0.1"
- has-property-descriptors "^1.0.2"
+ object-inspect "^1.13.4"
-setprototypeof@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
- integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+side-channel-map@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
+ integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
-side-channel@^1.0.6:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
- integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
+side-channel-weakmap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
+ integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
dependencies:
- call-bind "^1.0.7"
+ call-bound "^1.0.2"
es-errors "^1.3.0"
- get-intrinsic "^1.2.4"
- object-inspect "^1.13.1"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
+ side-channel-map "^1.0.1"
-statuses@2.0.1, statuses@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
- integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+side-channel@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab"
+ integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==
+ dependencies:
+ es-errors "^1.3.0"
+ object-inspect "^1.13.4"
+ side-channel-list "^1.0.1"
+ side-channel-map "^1.0.1"
+ side-channel-weakmap "^1.0.2"
+
+statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
+ integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
-toidentifier@1.0.1:
+toidentifier@~1.0.1:
version "1.0.1"
- resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+ resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
-type-is@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.0.tgz#7d249c2e2af716665cc149575dadb8b3858653af"
- integrity sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==
+type-is@^2.0.1, type-is@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz#71d1a7053293582e16ac9f3ebaf1ab9aa49e5570"
+ integrity sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==
dependencies:
- content-type "^1.0.5"
+ content-type "^2.0.0"
media-typer "^1.1.0"
mime-types "^3.0.0"
-type-is@~1.6.18:
- version "1.6.18"
- resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
- integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
- dependencies:
- media-typer "0.3.0"
- mime-types "~2.1.24"
-
-unpipe@1.0.0, unpipe@~1.0.0:
+unpipe@~1.0.0:
version "1.0.0"
- resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
- integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
-
-utils-merge@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
- integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+ resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
-vary@~1.1.2:
+vary@^1.1.2:
version "1.1.2"
- resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
- integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+ resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
wrappy@1:
version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==