From 0966bb0366c7874490e087dc51fae440cdf39ccc Mon Sep 17 00:00:00 2001
From: nat <98536588+natalie-a-1@users.noreply.github.com>
Date: Mon, 7 Sep 2026 22:27:40 -0500
Subject: [PATCH] Replace Meta-Scraper with Lucci Pay native ChatGPT checkout
pilot
---
.env.example | 20 +
.github/workflows/ci.yml | 17 +
.gitignore | 43 +-
.prettierignore | 4 +
.prettierrc.json | 1 +
.vscode-test.mjs | 5 -
.vscodeignore | 21 -
CHANGELOG.md | 9 -
PUBLISHING.md | 215 --
README.md | 113 +-
TESTING.md | 170 --
THIRD_PARTY_NOTICES.md | 31 +
config/merchants.example.json | 15 +
docs/integration.md | 107 +
docs/platform-limits.md | 17 +
docs/validation.md | 32 +
eslint.config.mjs | 25 -
examples/merchant-client.ts | 54 +
examples/request-payment.ts | 31 +
extension.js | 164 --
images/icon.png | Bin 4078 -> 0 bytes
images/output.gif | Bin 15553882 -> 0 bytes
jsconfig.json | 13 -
package-lock.json | 4153 +++++++++++----------------
package.json | 110 +-
scripts/build-preview-harness.mjs | 29 +
scripts/build-widget.mjs | 22 +
server/app.ts | 156 +
server/auth.ts | 41 +
server/config.ts | 128 +
server/errors.ts | 18 +
server/index.ts | 28 +
server/mcp.ts | 267 ++
server/payments.ts | 343 +++
server/processor.ts | 186 ++
server/store.ts | 92 +
shared/contracts.ts | 119 +
test-image/README.md | 20 -
test-image/add-test-metadata.js | 80 -
test-image/download-test-image.html | 90 -
test-image/example-usage.js | 78 -
test/extension.test.js | 15 -
tests/auth.test.ts | 81 +
tests/http.test.ts | 138 +
tests/payments.test.ts | 263 ++
tests/processor.test.ts | 173 ++
tsconfig.json | 16 +
tsconfig.server.json | 11 +
web/PaymentSheet.tsx | 547 ++++
web/bridge.ts | 50 +
web/host.d.ts | 10 +
web/index.tsx | 119 +
web/styles.css | 758 +++++
53 files changed, 5759 insertions(+), 3489 deletions(-)
create mode 100644 .env.example
create mode 100644 .github/workflows/ci.yml
create mode 100644 .prettierignore
create mode 100644 .prettierrc.json
delete mode 100644 .vscode-test.mjs
delete mode 100644 .vscodeignore
delete mode 100644 CHANGELOG.md
delete mode 100644 PUBLISHING.md
delete mode 100644 TESTING.md
create mode 100644 THIRD_PARTY_NOTICES.md
create mode 100644 config/merchants.example.json
create mode 100644 docs/integration.md
create mode 100644 docs/platform-limits.md
create mode 100644 docs/validation.md
delete mode 100644 eslint.config.mjs
create mode 100644 examples/merchant-client.ts
create mode 100644 examples/request-payment.ts
delete mode 100644 extension.js
delete mode 100644 images/icon.png
delete mode 100644 images/output.gif
delete mode 100644 jsconfig.json
create mode 100644 scripts/build-preview-harness.mjs
create mode 100644 scripts/build-widget.mjs
create mode 100644 server/app.ts
create mode 100644 server/auth.ts
create mode 100644 server/config.ts
create mode 100644 server/errors.ts
create mode 100644 server/index.ts
create mode 100644 server/mcp.ts
create mode 100644 server/payments.ts
create mode 100644 server/processor.ts
create mode 100644 server/store.ts
create mode 100644 shared/contracts.ts
delete mode 100644 test-image/README.md
delete mode 100644 test-image/add-test-metadata.js
delete mode 100644 test-image/download-test-image.html
delete mode 100644 test-image/example-usage.js
delete mode 100644 test/extension.test.js
create mode 100644 tests/auth.test.ts
create mode 100644 tests/http.test.ts
create mode 100644 tests/payments.test.ts
create mode 100644 tests/processor.test.ts
create mode 100644 tsconfig.json
create mode 100644 tsconfig.server.json
create mode 100644 web/PaymentSheet.tsx
create mode 100644 web/bridge.ts
create mode 100644 web/host.d.ts
create mode 100644 web/index.tsx
create mode 100644 web/styles.css
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..bc74af5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,20 @@
+# Local simulation; no credentials and no real charges.
+PAYMENT_MODE=demo
+PORT=4242
+PUBLIC_BASE_URL=http://localhost:4242
+DATABASE_PATH=./data/lucci-pay.sqlite
+
+# Native payment-sheet testing also requires OpenAI beta access.
+# PAYMENT_MODE=test
+# CHATGPT_CHECKOUT_ENABLED=true
+# STRIPE_SECRET_KEY=sk_test_...
+# MERCHANTS_FILE=./config/merchants.json
+# AUTH_ISSUER=https://your-identity-provider.example
+# AUTH_JWKS_URL=https://your-identity-provider.example/.well-known/jwks.json
+# AUTH_AUDIENCE=https://pay.your-domain.example/mcp
+# PUBLIC_BASE_URL=https://pay.your-domain.example
+
+# Live mode additionally requires approved commerce access.
+# PAYMENT_MODE=live
+# LIVE_PAYMENTS_APPROVED=true
+# STRIPE_SECRET_KEY=sk_live_...
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..35be733
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,17 @@
+name: Validate Lucci Pay
+on:
+ push:
+ pull_request:
+permissions:
+ contents: read
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+ cache: npm
+ - run: npm ci
+ - run: npm run check
diff --git a/.gitignore b/.gitignore
index da9badb..9ec2a4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,40 +1,9 @@
-# Dependency directories
node_modules/
-.npm
-
-# Build artifacts
-out/
dist/
-*.vsix
-
-# VS Code specific
-.vscode-test/
-.vscode/
-*.vsix
-
-# Logs
-logs
-*.log
-npm-debug.log*
-
-# Environment
+data/
.env
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-# Operating System Files
-.DS_Store
-Thumbs.db
-
-# Test images that contain personal data
-test-image/*.jpg
-test-image/*.jpeg
-test-image/*.png
-test-image/*.gif
-test-image/*.webp
-test-image/*.tiff
-
-# Exiftool temp files
-*_original
\ No newline at end of file
+.env.*
+!.env.example
+config/merchants.json
+*.log
+coverage/
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..226a39e
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,4 @@
+node_modules
+dist
+data
+package-lock.json
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..4926aa8
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1 @@
+{ "singleQuote": true, "trailingComma": "all", "printWidth": 100 }
diff --git a/.vscode-test.mjs b/.vscode-test.mjs
deleted file mode 100644
index cfe906b..0000000
--- a/.vscode-test.mjs
+++ /dev/null
@@ -1,5 +0,0 @@
-import { defineConfig } from '@vscode/test-cli';
-
-export default defineConfig({
- files: 'test/**/*.test.js',
-});
diff --git a/.vscodeignore b/.vscodeignore
deleted file mode 100644
index d756798..0000000
--- a/.vscodeignore
+++ /dev/null
@@ -1,21 +0,0 @@
-.vscode/**
-.vscode-test/**
-test/**
-.gitignore
-.yarnrc
-**/jsconfig.json
-**/*.map
-**/eslint.config.mjs
-**/.vscode-test.*
-
-# Development & documentation files not needed in the package
-PUBLISHING.md
-TESTING.md
-test-image/**
-.git/**
-CHANGELOG.md
-
-# Additional build artifacts to exclude
-**/*.vsix
-
-images/output.gif
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 136f296..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Change Log
-
-All notable changes to the "metascraper" extension will be documented in this file.
-
-Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
-
-## [Unreleased]
-
-- Initial release
\ No newline at end of file
diff --git a/PUBLISHING.md b/PUBLISHING.md
deleted file mode 100644
index 142ba09..0000000
--- a/PUBLISHING.md
+++ /dev/null
@@ -1,215 +0,0 @@
-#
Publishing Guide for Meta-Scraper
-
-
-
-[](https://marketplace.visualstudio.com/items?itemName=natalie-a-1.metascraper)
-[](LICENSE)
-
-
-
-This guide provides detailed instructions for packaging and publishing the Meta-Scraper extension to the Visual Studio Code Marketplace.
-
----
-
-## š Table of Contents
-
-- [Prerequisites](#-prerequisites)
-- [Packaging the Extension](#-packaging-the-extension)
-- [Testing Before Publishing](#-testing-before-publishing)
-- [Publishing to VS Code Marketplace](#-publishing-to-vs-code-marketplace)
-- [Managing Published Extensions](#-managing-published-extensions)
-- [Automating Publishing](#-automating-publishing)
-- [Resources](#-resources)
-
----
-
-## š§ Prerequisites
-
-Before you can publish the extension, you'll need to set up a few things:
-
-
-
-
- - Install the VS Code Extension Manager globally:
-
npm install -g @vscode/vsce
-
- - Create a Microsoft account if you don't have one
- - Create a Personal Access Token (PAT):
-
- - Go to Azure DevOps
- - Sign in with your Microsoft account
- - Select your profile icon ā Personal access tokens
- - Create a new token with "Marketplace (publish)" scope
- - ā ļø Save this token securely - you'll need it for publishing
-
-
-
-
-
-
----
-
-## š¦ Packaging the Extension
-
-Creating a `.vsix` package file is the first step for distribution:
-
-```bash
-# Navigate to your extension directory if needed
-cd meta-scraper
-
-# Create the package
-vsce package
-```
-
-This will generate a file named `metascraper-0.0.1.vsix` (or similar with your version number) in the current directory.
-
----
-
-## š§Ŗ Testing Before Publishing
-
-Always test your packaged extension before publishing:
-
-
-
-
- - In VS Code, open the Extensions view (
Ctrl+Shift+X)
- - Click the "..." menu in the top-right corner
- - Select "Install from VSIX..."
- - Choose your newly created .vsix file
- - Verify all functionality works as expected
- - Check that the extension icon displays correctly
- - Ensure the README is formatted properly
-
-
-
-
-See the [TESTING.md](TESTING.md) file for detailed testing procedures.
-
----
-
-## š Publishing to VS Code Marketplace
-
-When you're ready to publish:
-
-### Step 1: Create a Publisher Account
-
-
-
-
- - Go to the VS Code Marketplace Publishers page
- - Sign in with your Microsoft account
- - Click "New Publisher"
- - Fill in the details:
-
- - Publisher ID: "natalie-a-1" (must match package.json)
- - Display Name: Your preferred display name
- - Description: Brief bio or description
-
-
-
-
-
-
-### Step 2: Login and Publish
-
-```bash
-# Login with your publisher name and PAT
-vsce login natalie-a-1
-# You'll be prompted for your Personal Access Token
-
-# Publish the extension
-vsce publish
-```
-
-If successful, your extension will be available on the marketplace within a few minutes!
-
-### For Future Updates
-
-When you make changes and want to publish a new version:
-
-```bash
-# Increment version (choose one)
-vsce publish patch # 0.0.1 -> 0.0.2 (bug fixes)
-vsce publish minor # 0.0.1 -> 0.1.0 (new features)
-vsce publish major # 0.0.1 -> 1.0.0 (breaking changes)
-```
-
----
-
-## š Managing Published Extensions
-
-### Updating Extension Details
-
-You can update your extension's marketplace page information:
-
-1. Update your `README.md` with new information
-2. Change the `package.json` for metadata updates
-3. Re-publish using the steps above
-
-### Unpublishing an Extension
-
-If needed, you can remove your extension from the marketplace:
-
-```bash
-vsce unpublish natalie-a-1.metascraper
-```
-
-ā ļø **Warning**: Unpublishing an extension can be disruptive to users. Consider updating instead of removing.
-
----
-
-## āļø Automating Publishing
-
-For more efficient workflows, consider setting up GitHub Actions to automatically publish your extension when you create a new release:
-
-
-Click to view GitHub Actions workflow file example
-
-```yaml
-name: Publish Extension
-
-on:
- release:
- types: [created]
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
- with:
- node-version: 16
- - run: npm ci
- - name: Publish to Visual Studio Marketplace
- uses: HaaLeo/publish-vscode-extension@v1
- with:
- pat: ${{ secrets.VS_MARKETPLACE_TOKEN }}
- registryUrl: https://marketplace.visualstudio.com
-```
-
-
-
----
-
-## š Resources
-
-- [VS Code Publishing Extensions Guide](https://code.visualstudio.com/api/working-with-extensions/publishing-extension)
-- [vsce Command Line Tool Documentation](https://github.com/microsoft/vscode-vsce)
-- [VS Code Marketplace](https://marketplace.visualstudio.com/)
-- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
-
----
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index cf8ef06..77f5c6c 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,98 @@
-#
Meta-Scraper: Image Metadata Cleaner
+# Lucci Pay
-[](https://marketplace.visualstudio.com/items?itemName=NatalieHill.metascraper)
-[](LICENSE)
+Payment requests from participating apps, reviewed and paid inside ChatGPT.
-Meta-Scraper is a Visual Studio Code extension for inspecting and removing image metadata, helping you avoid leaking sensitive information when sharing images. It currently has **250+ installs** on the Visual Studio Code Marketplace.
+This is a new project replacing the Meta-Scraper VS Code extension. The UI takes its cues from Wallet: a compact sheet, an identifiable merchant, an exact total, one primary action, and a clear receipt. The real checkout uses ChatGPTās native payment sheet with Apple Pay requested as a supported method. Payment details and authentication belong to the host.
-## Demo
+**Status: working local demo and implementation for a gated native-checkout pilot. Not a launched payment service.**
-
+## What is possible today
-## Features
+| Capability | Status |
+| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
+| Merchant backend creates a payment request | Implemented with authentication, user binding and idempotency |
+| Review request and receipt in ChatGPT | Implemented with MCP Apps |
+| Native ChatGPT payment sheet | `window.openai.requestCheckout` integration; requires OpenAIās private-beta enablement |
+| Apple Pay | Declared in the native sheetās supported methods; actual availability depends on OpenAI, PSP, account and device |
+| Stripe payment processing | Scoped shared payment token adapter implemented; requires PSP credentials and end-to-end partner testing |
+| Arbitrary plugin subscriptions, digital goods, credits, or service fees | Not enabled; current general commerce approval covers physical goods |
+| Universal access to other apps or automatic charges | Not available; requesting apps must integrate, and the buyer explicitly approves checkout |
-- View metadata embedded in an image
-- Remove metadata in a single action
-- Supports `.jpg`, `.jpeg`, `.png`, `.gif`, `.tiff`, `.webp`
-- Fast processing with real-time feedback
-- All processing is done locally; no data leaves your machine
+OpenAIās checkout reference lists Apple Pay in the native payment-provider schema. It does **not** establish that every ChatGPT installation can open Apple Pay. Its UI guide describes private-beta access, while the submission guidelines restrict embedded third-party checkout. This project therefore has **no external checkout fallback, embedded Stripe card form, ApplePaySession workaround, or simulated biometric prompt**. When native checkout is unavailable, the action is disabled.
-
-
Right-click any image file in VS Code to access Meta-Scraper actions.
-
+The initial merchant contract supports **pre-priced physical-goods pickup orders**, with USD, CAD, GBP and EUR totals in minor units. Shipping/address changes, inventory reservation, subscriptions, refunds, payouts and an unrestricted payment network are outside this first implementation. See [platform limits](docs/platform-limits.md).
-## Why Cleaning Image Metadata Matters
+## Run the demo
-Modern image files can contain hidden metadata that exposes personal or sensitive details:
+Node.js 24 is required for the built-in SQLite store.
-| Metadata Type | Privacy Risk |
-|--------------------|------------------------------------------------------------|
-| GPS location | Reveals the exact place where the photo was taken |
-| Device information | Exposes camera/phone model and unique identifiers |
-| Creation time | Shows precisely when the photo was taken |
-| Software details | Reveals which tools were used to edit the image |
-| Author information | Can include your name, email, or other personal details |
+```sh
+npm ci
+npm run dev
+```
-Examples of potential issues:
+Open [the local preview](http://localhost:4242/preview). Try a successful demo payment, a decline, retry and cancellation. Every simulated payment is labeled **Demo** and moves no money.
-- Revealing your home or workplace location
-- Leaking details in screenshots or documents shared publicly
+```sh
+npm run demo:request # an example requesting-app backend; keep the server running
+npm run check # type checking, payment/API tests and production build
+npm start # run the compiled app after npm run build
+```
+
+The demo binds to loopback. It uses fictional shared data and a public demo key. Never use demo mode for real orders or customer information.
+
+## Connect to ChatGPT
+
+1. Run the server and expose `/mcp` through an HTTPS tunnel for development. Set `PUBLIC_BASE_URL` to the tunnel origin and restart so the host allowlist matches. A tunnel pointing at loopback can still reach the demo server.
+2. In ChatGPTās app settings, enable Developer Mode and add the HTTPS MCP endpoint, such as `https://your-dev-domain/mcp`. Current plugin documentation uses the newer **Plugins ā Connect and test** terminology; labels can vary by client.
+3. In demo mode, ask: āShow me the Lucci Pay demo.ā The `create_demo_payment_request` tool renders the sheet.
+4. For a participating appās request, enable both integrations and ask to review its returned request ID with Lucci Pay.
+5. Refresh the app connection after changing tool descriptors or UI resource metadata.
+
+This repository does not deploy a public endpoint or register a marketplace listing. ChatGPTās actual native payment sheet, iOS behavior, Apple Pay, 3DS and Stripe sandbox settlement still need to be exercised in an enabled account.
+
+## Native checkout setup
+
+Use `.env.example` and `config/merchants.example.json`. Real credentials belong in environment variables or a secret manager; `config/merchants.json`, local databases and `.env` files are ignored by git.
+
+- Obtain OpenAI approval for the intended marketplace and commerce category. `CHATGPT_CHECKOUT_ENABLED=true` records your deployment choice; it cannot grant beta access.
+- Configure an OAuth 2.1 identity provider, public discovery, PKCE, a supported client registration method, JWKS and the resource audience. This server implements the protected resource side, not an authorization server. See [authentication](docs/integration.md#identity-and-account-linking).
+- Register each merchant with its PSP-assigned native-checkout merchant identifier, optional Stripe Connect account, real policy URLs, and a hash of a strong merchant API key.
+- Confirm with Stripe that the token format, preview API version and connected-account routing match your OpenAI integration. The adapter uses Stripeās documented `shared_payment_granted_token` flow.
+- Set `PAYMENT_MODE=test` with a **test** key before exercising the real host. This mode accepts only scoped SPT tokens and never exposes demo tools.
+- Enable live mode only after the pilot is validated. It requires a live Stripe key, native checkout enabled and `LIVE_PAYMENTS_APPROVED=true`. These flags are deployment gates, not proof of commercial approval.
+
+Initial account connection may require the identity providerās sign-in flow. Once connected, the purchase flow itself stays in ChatGPT. The app does not read Apple Wallet contents or reuse a payment method from another app without an authorized provider token.
+
+## Architecture
+
+```text
+server/ Merchant API, OAuth verification, MCP tools, payment lifecycle, SQLite, Stripe
+web/ React payment sheet and standard MCP Apps bridge
+shared/ Request schemas and checkout types
+examples/ Small merchant-backend client and runnable request example
+tests/ Lifecycle, processor, auth and HTTP/MCP integration checks
+docs/ Integration contract, platform limits and validation notes
+```
+
+The primary app shape is `interactive-decoupled`: `show_payment_request` renders the widget; status and mutation tools update it without replacing the UI. Tool/resource wiring is adapted from OpenAIās **MCP App Basics** example at commit `18cc38e78a968712c357bacdc3c79fead5bfc6b4`. The widget uses the core `App` bridge; `callServerTool` wraps standard `tools/call`. The `window.openai` extension is used only for native checkout and optional dismissal.
+
+Prices are immutable once requested. The server calculates totals, binds the request to a payer and registered merchant, and persists each attempt before contacting Stripe. Retries reuse the same provider idempotency key. Unknown outcomes remain `processing`; only an authoritative matching payment result produces a receipt. Raw payment tokens and buyer details are not persisted or echoed into tool output.
+
+Use one server instance with a persistent SQLite volume for the pilot. Run behind HTTPS with the configured Host preserved. Load balancing, identity-based distributed rate limiting, operator reconciliation, storage backups and fulfillment integration need an operational design before public launch.
+
+## Documentation
+
+- [Participating-app integration](docs/integration.md)
+- [Platform limits and Apple Pay](docs/platform-limits.md)
+- [Validation and remaining host checks](docs/validation.md)
+- [Third-party notices](THIRD_PARTY_NOTICES.md)
+
+Primary references checked for this implementation:
+
+- [OpenAI checkout API](https://developers.openai.com/plugins/build/monetization)
+- [OpenAI MCP UI guide](https://developers.openai.com/plugins/build/chatgpt-ui)
+- [OpenAI MCP server](https://developers.openai.com/plugins/build/mcp-server), [tool design](https://developers.openai.com/plugins/plan/tools), [UI reference](https://developers.openai.com/plugins/reference)
+- [OpenAI plugin guidelines](https://developers.openai.com/plugins/app-guidelines) and [authentication](https://developers.openai.com/plugins/build/auth)
+- [Stripe shared payment tokens](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens?agent-seller=seller)
+- [Apple Pay on the Web](https://developer.apple.com/documentation/applepayontheweb)
diff --git a/TESTING.md b/TESTING.md
deleted file mode 100644
index 2b6f53b..0000000
--- a/TESTING.md
+++ /dev/null
@@ -1,170 +0,0 @@
-#
Testing Guide for Meta-Scraper
-
-
-
-[](https://marketplace.visualstudio.com/items?itemName=natalie-a-1.metascraper)
-[](LICENSE)
-
-
-
-This comprehensive guide will help you effectively test the Meta-Scraper extension to ensure all features are working correctly.
-
----
-
-## š Table of Contents
-
-- [Testing Installation Methods](#-testing-installation-methods)
-- [Testing with Sample Images](#-testing-with-sample-images)
-- [Feature Testing Checklist](#-feature-testing-checklist)
-- [Troubleshooting](#-troubleshooting)
-- [Advanced Testing](#-advanced-testing)
-- [Reporting Issues](#-reporting-issues)
-
----
-
-## š Testing Installation Methods
-
-### Method 1: Install from VSIX package (Recommended for end-users)
-
-
-
-
- - Open VS Code
- - Press
Ctrl+Shift+X (or Cmd+Shift+X on Mac) to open Extensions
- - Click the "..." menu in the top-right corner
- - Select "Install from VSIX..."
- - Navigate to and select
metascraper-0.0.1.vsix
- - Restart VS Code after installation
-
-
-
-
-### Method 2: Run in Development Mode (For developers)
-
-
-
-
- - Open the project in VS Code:
-
code --new-window .
-
- - Press
F5 or go to Run > Start Debugging
- - This launches an "Extension Development Host" window
- - Test your extension in this development window
- - Changes to code require restarting the debugging session
-
-
-
-
----
-
-## š¼ļø Testing with Sample Images
-
-### Option 1: Use Your Own Images
-
-
-
-
- - Copy images into the
test-image directory
- - Right-click on an image and select "View Image Metadata"
- - Check what metadata exists in the image
- - Right-click again and select "Clean Image Metadata"
- - View metadata again to verify cleaning was successful
-
-
-
-
-### Option 2: Create Test Images with Metadata
-
-We've included a script that adds artificial test metadata to any image, perfect for testing the cleaning functionality:
-
-```bash
-# First install dependencies
-npm install
-
-# Then run the script on any image
-node test-image/add-test-metadata.js test-image/your-image.jpg
-```
-
-The script adds:
-- GPS coordinates
-- Camera information
-- Creation timestamps
-- Copyright details
-- Author information
-
-
-
-
-
----
-
-## ā
Feature Testing Checklist
-
-Use this checklist to verify all extension features are working properly:
-
-| Feature | Test Steps | Expected Result | Status |
-|---------|------------|-----------------|--------|
-| **View Metadata** | Right-click image ā "View Image Metadata" | JSON metadata displayed in new document | ⬠|
-| **Clean Metadata** | Right-click image ā "Clean Image Metadata" | Progress notification ā Success message | ⬠|
-| **Verify Cleaning** | View metadata after cleaning | Most/all metadata fields removed | ⬠|
-| **Image Integrity** | Open image after cleaning | Image appears identical to original | ⬠|
-| **Error: Non-Image** | Right-click non-image file ā try commands | Proper error message shown | ⬠|
-| **Error: Read-Only** | Test with read-only image | Graceful error handling | ⬠|
-| **Large Images** | Test with high-resolution image | Successfully processes without timeout | ⬠|
-
----
-
-## š§ Troubleshooting
-
-If you encounter issues during testing:
-
-1. **Command not appearing in context menu**
- - Ensure the extension is properly installed
- - Verify you're right-clicking on a supported image format
- - Try reloading the VS Code window
-
-2. **Metadata not fully removed**
- - Some specialized file formats may retain certain metadata
- - Check if the file is locked by another application
- - Try saving the image to a new location first
-
-3. **Performance issues with large files**
- - Processing very large images may take longer
- - Ensure your system has sufficient resources available
-
----
-
-## š¬ Advanced Testing
-
-For more thorough testing, try these advanced scenarios:
-
-- Test with multiple image formats (JPG, PNG, GIF, TIFF, WebP)
-- Test with unusually large image files (20MB+)
-- Test with images containing extensive metadata
-- Test the extension while other resource-intensive operations are running
-- Test with images in read-only locations or with restricted permissions
-
----
-
-## š Reporting Issues
-
-If you encounter bugs or have feature suggestions:
-
-1. Check existing [issues on GitHub](https://github.com/natalie-a-1/Meta-Scraper/issues) to see if it's already reported
-2. Create a new issue with:
- - A clear descriptive title
- - Detailed steps to reproduce
- - Expected vs. actual behavior
- - VS Code version and OS information
- - Screenshots or logs if available
-
----
-
-
\ No newline at end of file
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..746fe31
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,31 @@
+# Third-party notices
+
+MCP App Basics server and widget connection patterns are adapted from
+[openai/openai-apps-sdk-examples](https://github.com/openai/openai-apps-sdk-examples)
+at commit `18cc38e78a968712c357bacdc3c79fead5bfc6b4`.
+
+Original files consulted: `mcp_app_basics_node/src/server.ts` and
+`src/call-server-tool/App.tsx`. Demo-specific content was removed; the payment
+service and UI are new. The current version uses the core MCP Apps `App` API.
+
+MIT License
+
+Copyright (c) 2025 OpenAI
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/config/merchants.example.json b/config/merchants.example.json
new file mode 100644
index 0000000..e86bf7e
--- /dev/null
+++ b/config/merchants.example.json
@@ -0,0 +1,15 @@
+[
+ {
+ "id": "your-merchant",
+ "name": "Your merchant name",
+ "domain": "your-merchant.example",
+ "api_key_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
+ "provider_merchant_id": "profile_replace_with_psp_assigned_value",
+ "stripe_account": "acct_replace",
+ "category": "physical_goods",
+ "terms_url": "https://your-merchant.example/terms",
+ "privacy_url": "https://your-merchant.example/privacy",
+ "refund_url": "https://your-merchant.example/refunds",
+ "support_url": "https://your-merchant.example/support"
+ }
+]
diff --git a/docs/integration.md b/docs/integration.md
new file mode 100644
index 0000000..dc81520
--- /dev/null
+++ b/docs/integration.md
@@ -0,0 +1,107 @@
+# Participating-app integration
+
+A requesting app creates the order using its own trusted backend and gives ChatGPT the returned `request_id`. ChatGPT can show the request through Lucci Pay when the user has enabled that integration and asks to pay. MCP Apps does not give an arbitrary plugin permission to call another appās widget or access its credentials.
+
+## Request an order
+
+Authenticate with a **merchant backend key**, never a userās access token. The registered merchant determines the seller name, policy URLs, PSP merchant ID and Stripe account. Callers cannot override those fields.
+
+```http
+POST /v1/payment-requests
+Authorization: Bearer
+Content-Type: application/json
+Idempotency-Key: order-2026-123
+```
+
+```json
+{
+ "merchant_order_reference": "order-2026-123",
+ "payer_subject": "linked-payer-subject",
+ "currency": "USD",
+ "line_items": [
+ {
+ "id": "cup-1",
+ "name": "Everyday ceramic cup",
+ "quantity": 1,
+ "unit_amount": 2400,
+ "discount": 0,
+ "tax": 198
+ }
+ ],
+ "fulfillment": {
+ "type": "pickup",
+ "title": "Pick up in store",
+ "subtitle": "Your merchant location Ā· Ready in 2 hours"
+ },
+ "expires_in_seconds": 900
+}
+```
+
+`unit_amount` is per item. `discount` and `tax` are **whole-line** amounts, not per-unit amounts. Subtotal is `quantity Ć unit_amount ā discount`; total is subtotal plus tax. Supply tax and pickup terms after validating inventory and fulfillment in the requesting merchantās backend. Lucci Pay validates the arithmetic but is not a tax engine or inventory service. Maximum request size is 48 KB, 30 unique lines, 100 units per line and 10,000,000 minor units total. Expiry is 1ā30 minutes. Use only the four supported two-decimal currencies.
+
+```json
+{
+ "request_id": "pay_",
+ "mode": "test",
+ "status": "pending",
+ "expires_at": "",
+ "handoff": {
+ "tool": "show_payment_request",
+ "arguments": { "request_id": "pay_" }
+ }
+}
+```
+
+The handoff is data for the host to use; it is not an automatic cross-plugin invocation protocol. Return it as part of your appās MCP tool result after the user decides to review a purchase. Avoid sending the merchant API key or payer identity into chat. [merchant-client.ts](../examples/merchant-client.ts) is a small server-side integration example.
+
+Retry transient request creation failures with the **same** idempotency key and payload. Changing the amount, currency, payer or other request data under the same key returns `409 idempotency_conflict`. To replace an expired quote, issue a new order revision/key. An existing request is never silently repriced.
+
+## Identity and account linking
+
+The `payer_subject` must come from a trusted account-link mapping established by the merchant and your OAuth identity provider. It must exactly match the authenticated `sub` claim on the buyerās Lucci Pay access token. Do not accept a payer ID from arbitrary model text or let a merchant impersonate a buyer. Each read, cancel and completion verifies ownership. Merchant API keys cannot authenticate to user tools.
+
+This initial repository does not provide merchant self-service onboarding or a turnkey account-linking UI. Configure merchants and establish trusted identity mappings before a pilot. For a public multi-tenant service, introduce a pairwise linking API instead of sharing global user identifiers across merchants.
+
+The MCP server publishes RFC 9728 resource metadata at `/.well-known/oauth-protected-resource` and `/.well-known/oauth-protected-resource/mcp`. The external authorization server must support the current MCP OAuth flow, PKCE, discovery and a supported registration mechanism. Configure its audience as `PUBLIC_BASE_URL/mcp`. User JWTs require a valid RS256/ES256 signature, issuer, audience, subject, issuance time and expiry. Scopes:
+
+| Scope | Purpose |
+| --------------- | ---------------------------------- |
+| `payments:read` | Review owned requests and receipts |
+| `payments:pay` | Confirm or cancel an owned request |
+
+The SDK version used here exposes security metadata through `_meta.securitySchemes`. Read-only tools are model-visible; payment completion and cancellation are private/app-visible. **Visibility is not authorization:** OAuth, ownership and the scoped PSP token provide the server-side controls.
+
+## Native confirmation
+
+1. `show_payment_request` returns the immutable checkout session and review UI.
+2. The user presses Pay. The widget calls the feature-detected `window.openai.requestCheckout(session)` directly from that gesture.
+3. The host collects the payment method and confirmation, then calls `complete_checkout` with `checkout_session_id`, optional `buyer`, and `payment_data: { "token": "spt_...", "provider": "stripe" }`.
+4. The server verifies the user, request, mode, merchant, token limits and expiry, persists an attempt, and submits the token to Stripe on the registered merchant account. It uses `payment_method_data[shared_payment_granted_token]`, exact server totals and a stable idempotency key.
+5. The widget retrieves authoritative status after the host returns. A resolved browser promise alone never marks the order paid.
+
+The optional buyer shape currently accepts `first_name`, `last_name`, and `email`, and the token envelope deliberately rejects unknown payment fields. Confirm the actual partner payload and pickup-session schema during the native pilot; adapt versioned schemas if the enabled host/PSP contract differs. Never loosen this into a raw-card endpoint.
+
+No tokens or buyer details are stored in SQLite, logs or model-visible responses. Stripe API access uses the fixed provider origin, a timeout and API version `2026-04-22.preview`. Keep this version aligned with your granted-token integration.
+
+## Fulfill after verified payment
+
+```http
+GET /v1/payment-requests/pay_
+Authorization: Bearer
+```
+
+The response contains the merchant order reference, status, environment and receipt, with no payer identity or payment credentials. Fulfill exactly once only when `status` is `paid`, the environment matches your order, and the receipt amount/currency match your merchant record. Use the stable receipt ID as your fulfillment idempotency key.
+
+This version uses authenticated status retrieval instead of outbound webhooks. Retrieving a `processing` request with a known PaymentIntent refreshes its provider status. Do not treat a demo/test receipt as a live payment.
+
+| State | Meaning / permitted next action |
+| ----------------------- | --------------------------------------------------------------------------------------------- |
+| `pending` | Awaiting the user; pay, cancel or expire |
+| `processing` | Provider result uncertain or authentication pending; check status, never start another charge |
+| `paid` | Authoritative receipt; repeated completion returns the same receipt |
+| `failed` | Definite decline or invalid authorization; a new token may retry before expiry |
+| `cancelled` / `expired` | Closed; request a new quote |
+
+After a network timeout with no recorded provider ID, the native host can retry the original token and reuse the existing attempt/key for up to 23 hours. The server stores only the token hash. After the key retention window, operator reconciliation is required. Do not reset processing state or mint a new key to āfixā uncertainty. A `requires_3ds` response keeps the request locked; the host must resolve authentication, or the operator must reconcile it. No redirect fallback is implemented.
+
+Refunds and fulfillment support belong to the merchant. This app does not custody money, maintain a stored balance, or provide peer-to-peer transfers.
diff --git a/docs/platform-limits.md b/docs/platform-limits.md
new file mode 100644
index 0000000..4879132
--- /dev/null
+++ b/docs/platform-limits.md
@@ -0,0 +1,17 @@
+# Platform limits and Apple Pay
+
+Research checked September 8, 2026. Recheck before launch because the native checkout APIs and commercial eligibility are evolving.
+
+The closest supported implementation of the requested experience is **ChatGPTās native payment sheet**, with Apple Pay as a supported method. OpenAIās [checkout reference](https://developers.openai.com/plugins/build/monetization) documents `window.openai.requestCheckout`, the payment-provider schema and the `complete_checkout` callback. Apple Pay is explicitly present in the exampleās supported payment methods. This repository uses those fields; it does not fabricate an Apple Wallet sheet.
+
+Native checkout remains a **private beta for selected marketplaces**. Setting a configuration flag or adding Appleās JavaScript cannot unlock it. OpenAIās [UI guide](https://developers.openai.com/plugins/build/chatgpt-ui#use-the-chatgpt-payment-sheet) confirms this limitation. Current general commerce approval covers **physical goods**; digital app credits, subscriptions and service payments require separate explicit enablement. This is a platform constraint on the original universal app-payments concept, not an implemented digital-goods capability.
+
+The [submission guidelines](https://developers.openai.com/plugins/app-guidelines#checkout) prohibit embedding other third-party checkout solutions in plugin UI. The checkout and UI guides also describe using payment methods already saved with a merchant, while the general submission wording still calls external checkout the default. We did not use the saved-method flow to sidestep that ambiguity or the userās seamless setup goal. The chosen implementation is the explicitly documented native flow.
+
+Appleās [web payment APIs](https://developer.apple.com/documentation/applepayontheweb) require Appleās supported browser/merchant integration and do not establish compatibility with ChatGPTās controlled iframe. The requesting app cannot control ChatGPTās parent frame, permissions, native entitlements or Wallet. No direct `ApplePaySession`, embedded PSP frame, fake Apple Pay brand button, Face ID animation or double-click-side-button instruction is used. When an enabled host displays genuine Apple Pay, Apple and the host handle authentication.
+
+The request API enables **participating merchants** to integrate. There is no documented universal ability for this app to intercept payment requests from every installed plugin. Both integrations must be available to the host, and the requesting service must explicitly implement the handoff and trusted account mapping. OpenAI adds host-owned merchant/connector context to the native sheet; marketplace branding and per-seller settlement must be validated with OpenAI and the PSP rather than overwritten in client code.
+
+Stripeās old app-monetization documentation link now redirects to [MPP-based MCP monetization](https://docs.stripe.com/agentic-commerce/monetize-mcp). That is a different agent-wallet payment flow and does not demonstrate native Apple Pay in ChatGPT. This implementation instead uses the [documented seller shared-payment-token API](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens?agent-seller=seller) behind OpenAIās `complete_checkout` contract. The exact native host/Stripe token envelope still needs partner validation.
+
+The code can be reviewed and the local demo can be run now. Public launch depends on OpenAI/PSP access, merchant and identity setup, the enabled-host integration test, and normal merchant fulfillment operations. The PR does not claim those external prerequisites have been completed.
diff --git a/docs/validation.md b/docs/validation.md
new file mode 100644
index 0000000..00284da
--- /dev/null
+++ b/docs/validation.md
@@ -0,0 +1,32 @@
+# Validation
+
+Checked September 8, 2026 on Node.js 24.19.0.
+
+## Completed
+
+`npm run check` passes:
+
+- TypeScript checks for server, widget, examples and tests.
+- **22 tests**, all passing.
+- Self-contained React widget bundle and compiled MCP/HTTP server.
+
+The tests cover authoritative amount calculation, tampering and invalid amounts, duplicate request keys, payer/merchant isolation, account-routing changes, cancellation, expiry, retry after decline, unknown payment outcomes, persistent recovery after restart, the provider idempotency retention window, concurrent confirmations, required 3DS state, demo isolation, signed JWT validation, missing scopes, token constraints and receipt verification. A delayed UI result cannot overwrite a newer paid receipt because state versions are monotonic.
+
+A real MCP SDK client exercises initialization, discovery, resource retrieval, demo request creation, payment review, simulation, and receipt retrieval over a locally running HTTP server. A separate native-tool contract test verifies private tool metadata, scope checks, token-only inputs and output-schema conformance using a fake PSP. Stripe HTTP requests are tested through an injected transport; these tests never send a real payment.
+
+The compiled demo server also started successfully. `git diff --check` passed.
+
+## Not completed
+
+- **Visual browser review:** this environmentās Cloud Browser policy blocked the local preview and local-file navigation. No screenshot or mobile browser verification is claimed. Open `/preview` locally to review the layout. `node scripts/build-preview-harness.mjs` also creates an offline UI fixture after a build; that fixture uses simulated browser responses and is not an end-to-end backend test.
+- **ChatGPT host loop:** no enabled native-checkout account was available for validation. The MCP protocol and response schemas were tested locally, not in ChatGPTās actual payment sheet.
+- **Real PSP sandbox/live settlement:** no Stripe credentials or merchant account were supplied. The exact partner token/buyer envelope, pickup schema, Apple Pay eligibility, 3DS handling and Stripe account routing require validation with the enabled OpenAI/PSP integration.
+- **Production:** no hosting, merchant registration, OAuth authorization server, public policy pages, marketplace approval or launch was performed.
+
+Local validation reaches the working-server and MCP protocol levels. It does not establish native Apple Pay availability, payment-provider acceptance, visual readiness or public-launch readiness.
+
+## Before enabling a real pilot
+
+Run test-mode purchases inside the enabled ChatGPT app on the target iOS and web clients. Verify the genuine native sheet and Apple Pay availability; complete success, decline, cancellation and 3DS cases; repeat a confirmation during a timeout; verify matching Stripe and merchant receipts; then check keyboard navigation, narrow screens, dark mode and reduced motion. Confirm that an unsupported host disables checkout and that no purchase path opens an external site. Validate policy links separately.
+
+Do not enable live funds solely because automated tests pass. The remaining checks need the actual host, processor and registered merchant context.
diff --git a/eslint.config.mjs b/eslint.config.mjs
deleted file mode 100644
index db89148..0000000
--- a/eslint.config.mjs
+++ /dev/null
@@ -1,25 +0,0 @@
-import globals from "globals";
-
-export default [{
- files: ["**/*.js"],
- languageOptions: {
- globals: {
- ...globals.commonjs,
- ...globals.node,
- ...globals.mocha,
- },
-
- ecmaVersion: 2022,
- sourceType: "module",
- },
-
- rules: {
- "no-const-assign": "warn",
- "no-this-before-super": "warn",
- "no-undef": "warn",
- "no-unreachable": "warn",
- "no-unused-vars": "warn",
- "constructor-super": "warn",
- "valid-typeof": "warn",
- },
-}];
\ No newline at end of file
diff --git a/examples/merchant-client.ts b/examples/merchant-client.ts
new file mode 100644
index 0000000..3927cf9
--- /dev/null
+++ b/examples/merchant-client.ts
@@ -0,0 +1,54 @@
+import type { RequestInput } from '../shared/contracts.js';
+
+export interface MerchantClientOptions {
+ baseUrl: string;
+ apiKey: string;
+}
+export interface PaymentHandoff {
+ request_id: string;
+ status: string;
+ mode: string;
+ expires_at: string;
+ handoff: { tool: 'show_payment_request'; arguments: { request_id: string } };
+}
+/** Call from the requesting app's backend, using its authenticated order data. */
+export async function createPaymentRequest(
+ options: MerchantClientOptions,
+ order: RequestInput,
+): Promise {
+ const response = await fetch(`${options.baseUrl.replace(/\/$/, '')}/v1/payment-requests`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${options.apiKey}`,
+ 'Content-Type': 'application/json',
+ 'Idempotency-Key': order.merchant_order_reference,
+ },
+ body: JSON.stringify(order),
+ signal: AbortSignal.timeout(15_000),
+ redirect: 'error',
+ });
+ if (!response.ok)
+ throw new Error(
+ `Payment request failed (${response.status}). Retry the same order reference for transient failures.`,
+ );
+ return (await response.json()) as PaymentHandoff;
+}
+
+/** Fulfill only after this returns status=paid and mode matches your environment. */
+export async function getPaymentStatus(options: MerchantClientOptions, requestId: string) {
+ const response = await fetch(
+ `${options.baseUrl.replace(/\/$/, '')}/v1/payment-requests/${encodeURIComponent(requestId)}`,
+ {
+ headers: { Authorization: `Bearer ${options.apiKey}` },
+ signal: AbortSignal.timeout(15_000),
+ redirect: 'error',
+ },
+ );
+ if (!response.ok) throw new Error(`Status lookup failed (${response.status}).`);
+ return (await response.json()) as {
+ request_id: string;
+ status: string;
+ mode: string;
+ receipt?: unknown;
+ };
+}
diff --git a/examples/request-payment.ts b/examples/request-payment.ts
new file mode 100644
index 0000000..7ca6bb6
--- /dev/null
+++ b/examples/request-payment.ts
@@ -0,0 +1,31 @@
+import { randomUUID } from 'node:crypto';
+import { createPaymentRequest } from './merchant-client.js';
+
+const request = await createPaymentRequest(
+ {
+ baseUrl: process.env.LUCCI_PAY_URL ?? 'http://localhost:4242',
+ apiKey: process.env.LUCCI_MERCHANT_KEY ?? 'demo-merchant-key',
+ },
+ {
+ merchant_order_reference: `order-${randomUUID()}`,
+ payer_subject: process.env.LUCCI_PAYER_SUBJECT ?? 'demo-user',
+ currency: 'USD',
+ line_items: [
+ {
+ id: 'ceramic-cup',
+ name: 'Everyday ceramic cup',
+ quantity: 1,
+ unit_amount: 2400,
+ tax: 198,
+ discount: 0,
+ },
+ ],
+ fulfillment: {
+ type: 'pickup',
+ title: 'Pick up in store',
+ subtitle: 'Studio Supply Ā· Ready in 2 hours',
+ },
+ expires_in_seconds: 900,
+ },
+);
+console.log(JSON.stringify(request, null, 2));
diff --git a/extension.js b/extension.js
deleted file mode 100644
index a519963..0000000
--- a/extension.js
+++ /dev/null
@@ -1,164 +0,0 @@
-// The module 'vscode' contains the VS Code extensibility API
-// Import the module and reference it with the alias vscode in your code below
-const vscode = require('vscode');
-const { ExifTool } = require('exiftool-vendored');
-const path = require('path');
-const fs = require('fs');
-
-// This method is called when your extension is activated
-// Your extension is activated the very first time the command is executed
-
-/**
- * @param {vscode.ExtensionContext} context
- */
-function activate(context) {
-
- // Use the console to output diagnostic information (console.log) and errors (console.error)
- // This line of code will only be executed once when your extension is activated
- console.log('Metadata Scraper extension is now active');
-
- // Register the command to clean metadata from an image
- const cleanMetadataDisposable = vscode.commands.registerCommand('metascraper.cleanMetadata', async function (fileUri) {
- if (!fileUri) {
- // If command was triggered from command palette without a file
- vscode.window.showErrorMessage('Please right-click on an image file to clean its metadata.');
- return;
- }
-
- const filePath = fileUri.fsPath;
- const fileName = path.basename(filePath);
- const fileExt = path.extname(filePath).toLowerCase();
-
- // Check if the file is an image
- const supportedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.tiff', '.webp'];
- if (!supportedExtensions.includes(fileExt)) {
- vscode.window.showErrorMessage(`File "${fileName}" is not a supported image format.`);
- return;
- }
-
- try {
- // Show progress notification
- await vscode.window.withProgress({
- location: vscode.ProgressLocation.Notification,
- title: `Cleaning metadata from ${fileName}`,
- cancellable: false
- }, async (progress) => {
- // Initialize ExifTool
- const exiftool = new ExifTool();
-
- try {
- progress.report({ message: 'Reading metadata...' });
-
- // Read the current metadata (for informational purposes)
- const metadata = await exiftool.read(filePath);
- console.log('Original metadata:', metadata);
-
- progress.report({ message: 'Cleaning metadata...' });
-
- // Generate a temporary file path in the same directory
- const dirPath = path.dirname(filePath);
- const baseName = path.basename(filePath, fileExt);
- const tempFilePath = path.join(dirPath, `${baseName}-temp${fileExt}`);
-
- // Remove all metadata and write to the temp file
- await exiftool.write(
- filePath,
- { all: '' },
- ['-P', '-o', tempFilePath]
- );
-
- // Check if both files exist
- if (fs.existsSync(tempFilePath)) {
- // If original file exists, delete it first
- if (fs.existsSync(filePath)) {
- fs.unlinkSync(filePath);
- }
-
- // Then rename the temp file to the original name
- fs.renameSync(tempFilePath, filePath);
-
- vscode.window.showInformationMessage(`Successfully cleaned metadata from "${fileName}".`);
- } else {
- throw new Error('Failed to create cleaned image file');
- }
-
- // Close exiftool process
- await exiftool.end();
- } catch (err) {
- console.error('Error:', err);
- // Make sure to close exiftool process on error
- await exiftool.end();
- throw err;
- }
- });
- } catch (error) {
- vscode.window.showErrorMessage(`Failed to clean metadata: ${error.message}`);
- }
- });
-
- // Register the command to view metadata from an image
- const viewMetadataDisposable = vscode.commands.registerCommand('metascraper.viewMetadata', async function (fileUri) {
- if (!fileUri) {
- vscode.window.showErrorMessage('Please right-click on an image file to view its metadata.');
- return;
- }
-
- const filePath = fileUri.fsPath;
- const fileName = path.basename(filePath);
- const fileExt = path.extname(filePath).toLowerCase();
-
- // Check if the file is an image
- const supportedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.tiff', '.webp'];
- if (!supportedExtensions.includes(fileExt)) {
- vscode.window.showErrorMessage(`File "${fileName}" is not a supported image format.`);
- return;
- }
-
- try {
- // Show progress notification
- await vscode.window.withProgress({
- location: vscode.ProgressLocation.Notification,
- title: `Reading metadata from ${fileName}`,
- cancellable: false
- }, async (progress) => {
- // Initialize ExifTool
- const exiftool = new ExifTool();
-
- try {
- // Read the current metadata
- const metadata = await exiftool.read(filePath);
-
- // Create a virtual document to display metadata
- const metadataJson = JSON.stringify(metadata, null, 2);
- const metadataDoc = await vscode.workspace.openTextDocument({
- content: metadataJson,
- language: 'json'
- });
-
- // Show the document
- await vscode.window.showTextDocument(metadataDoc, { preview: true });
-
- // Close exiftool process
- await exiftool.end();
- } catch (err) {
- console.error('Error:', err);
- // Make sure to close exiftool process on error
- await exiftool.end();
- throw err;
- }
- });
- } catch (error) {
- vscode.window.showErrorMessage(`Failed to read metadata: ${error.message}`);
- }
- });
-
- context.subscriptions.push(cleanMetadataDisposable, viewMetadataDisposable);
-}
-
-// This method is called when your extension is deactivated
-function deactivate() {}
-
-module.exports = {
- activate,
- deactivate
-}
diff --git a/images/icon.png b/images/icon.png
deleted file mode 100644
index 798995a272b37230f64f2c5413f626fb16b5166a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 4078
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