pop-pay 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,35 +1,187 @@
1
- # pop-pay
1
+ [![npm version](https://img.shields.io/npm/v/pop-pay.svg)](https://www.npmjs.com/package/pop-pay) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![CI](https://github.com/TPEmist/pop-pay/actions/workflows/ci.yml/badge.svg)](https://github.com/TPEmist/pop-pay/actions/workflows/ci.yml) [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
2
2
 
3
- **Point One Percent** — Semantic Payment Guardrail for AI Agents.
3
+ <p align="center">
4
+ <picture>
5
+ <img src="https://raw.githubusercontent.com/TPEmist/Point-One-Percent/main/project_banner.png" alt="Point One Percent (AgentPay)" width="800">
6
+ </picture>
7
+ </p>
4
8
 
5
- > It only takes 0.1% of hallucination to drain 100% of your wallet.
9
+ # Point One Percent pop-pay
10
+ <p align="left"><i>it only takes <b>0.1%</b> of Hallucination to drain <b>100%</b> of your wallet.</i></p>
6
11
 
7
- TypeScript + Rust implementation of the pop-pay runtime security layer for AI agent commerce.
12
+ The runtime security layer for AI agent commerce. Card credentials are injected directly into the browser DOM via CDP they never enter the agent's context window. One hallucinated prompt can't drain a wallet it can't see.
8
13
 
9
- ## Features
14
+ ## Getting Started
10
15
 
11
- - **Vault**: AES-256-GCM encrypted credential storage with Rust native security layer
12
- - **Guardrails**: Keyword + LLM-based payment intent validation
13
- - **MCP Server**: Model Context Protocol server for AI agent integration
14
- - **Providers**: Stripe Issuing, BYOC (Bring Your Own Card), Mock
15
- - **Security Scan**: Prompt injection detection on checkout pages
16
+ Add pop-pay to your MCP client config:
16
17
 
17
- ## Installation
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "pop-pay": {
22
+ "command": "npx",
23
+ "args": ["-y", "pop-pay", "launch-mcp"],
24
+ "env": {
25
+ "POP_CDP_URL": "http://localhost:9222",
26
+ "POP_ALLOWED_CATEGORIES": "[\"aws\",\"cloudflare\"]",
27
+ "POP_MAX_PER_TX": "100.0",
28
+ "POP_MAX_DAILY": "500.0",
29
+ "POP_GUARDRAIL_ENGINE": "keyword"
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20MCP%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522pop-pay%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522pop-pay%2522%252C%2522launch-mcp%2522%255D%252C%2522env%2522%253A%257B%2522POP_CDP_URL%2522%253A%2522http%253A%252F%252Flocalhost%253A9222%2522%257D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20MCP%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522pop-pay%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522pop-pay%2522%252C%2522launch-mcp%2522%255D%252C%2522env%2522%253A%257B%2522POP_CDP_URL%2522%253A%2522http%253A%252F%252Flocalhost%253A9222%2522%257D%257D) [<img src="https://img.shields.io/badge/Cursor-Cursor?style=flat-square&label=Install%20MCP%20Server&color=5C2D91" alt="Install in Cursor">](cursor://anysphere.cursor-deeplink/mcp/install?name=pop-pay&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInBvcC1wYXkiLCJsYXVuY2gtbWNwIl0sImVudiI6eyJQT1BfQ0RQX1VSTCI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTIyMiJ9fQ==)
37
+
38
+ <details>
39
+ <summary>Claude Code</summary>
18
40
 
19
41
  ```bash
20
- npm install pop-pay
42
+ claude mcp add pop-pay -- npx -y pop-pay launch-mcp
21
43
  ```
22
44
 
23
- ## Quick Start
45
+ With environment variables:
24
46
 
25
47
  ```bash
26
- # Initialize vault
27
- pop-init-vault
48
+ claude mcp add pop-pay \
49
+ -e POP_CDP_URL=http://localhost:9222 \
50
+ -e POP_ALLOWED_CATEGORIES='["aws","cloudflare"]' \
51
+ -e POP_MAX_PER_TX=100.0 \
52
+ -e POP_MAX_DAILY=500.0 \
53
+ -e POP_GUARDRAIL_ENGINE=keyword \
54
+ -- npx -y pop-pay launch-mcp
55
+ ```
56
+
57
+ </details>
58
+
59
+ <details>
60
+ <summary>Cursor</summary>
61
+
62
+ Add to `~/.cursor/mcp.json` using the standard config block above.
63
+
64
+ </details>
65
+
66
+ <details>
67
+ <summary>Windsurf</summary>
68
+
69
+ Add to `~/.codeium/windsurf/mcp_config.json` using the standard config block above.
70
+
71
+ </details>
72
+
73
+ <details>
74
+ <summary>VS Code (Copilot)</summary>
75
+
76
+ Add to `.vscode/mcp.json` in your project root using the standard config block above.
28
77
 
29
- # Launch MCP server
30
- pop-launch
78
+ </details>
79
+
80
+ <details>
81
+ <summary>OpenClaw / NemoClaw</summary>
82
+
83
+ Compatible with any MCP host. See the [Integration Guide](./docs/INTEGRATION_GUIDE.md) for setup instructions and System Prompt templates.
84
+
85
+ </details>
86
+
87
+ <details>
88
+ <summary>Docker</summary>
89
+
90
+ ```bash
91
+ docker-compose up -d
31
92
  ```
32
93
 
94
+ Runs the MCP server + headless Chromium with CDP. Mount your encrypted vault from the host. See `docker-compose.yml` for configuration.
95
+
96
+ </details>
97
+
98
+ ## Vault Setup
99
+
100
+ Credentials are stored in an AES-256-GCM encrypted vault — plaintext card data never touches disk.
101
+
102
+ ```bash
103
+ npx pop-init-vault
104
+ ```
105
+
106
+ **Passphrase mode** (recommended — protects against agents with shell access):
107
+
108
+ ```bash
109
+ npx pop-init-vault --passphrase # one-time setup
110
+ npx pop-unlock # run once before each MCP session
111
+ ```
112
+
113
+ `pop-unlock` derives the key from your passphrase and stores it in the OS keyring. The MCP server reads it automatically at startup.
114
+
115
+ ## MCP Tools
116
+
117
+ | Tool | Description |
118
+ |:---|:---|
119
+ | `request_virtual_card` | Issue a virtual card and inject credentials into the checkout page via CDP. |
120
+ | `request_purchaser_info` | Auto-fill billing/contact info (name, address, email, phone). |
121
+ | `request_x402_payment` | Pay for API calls via the x402 HTTP payment protocol. |
122
+ | `page_snapshot` | Scan a checkout page for hidden prompt injections or anomalies. |
123
+
124
+ ## Configuration
125
+
126
+ Core variables in `~/.config/pop-pay/.env`. See [ENV_REFERENCE.md](./docs/ENV_REFERENCE.md) for the full list.
127
+
128
+ | Variable | Default | Description |
129
+ |---|---|---|
130
+ | `POP_ALLOWED_CATEGORIES` | `["aws","cloudflare"]` | Approved vendor categories — see [Categories Cookbook](./docs/CATEGORIES_COOKBOOK.md) |
131
+ | `POP_MAX_PER_TX` | `100.0` | Max USD per transaction |
132
+ | `POP_MAX_DAILY` | `500.0` | Max USD per day |
133
+ | `POP_BLOCK_LOOPS` | `true` | Block hallucination/retry loops |
134
+ | `POP_AUTO_INJECT` | `true` | Enable CDP card injection |
135
+ | `POP_GUARDRAIL_ENGINE` | `keyword` | `keyword` (zero-cost) or `llm` (semantic) |
136
+
137
+ ### Guardrail Mode
138
+
139
+ | | `keyword` (default) | `llm` |
140
+ |---|---|---|
141
+ | **Mechanism** | Keyword matching on reasoning string | Semantic analysis via LLM |
142
+ | **Cost** | Zero — no API calls | One LLM call per request |
143
+ | **Best for** | Development, low-risk workflows | Production, high-value transactions |
144
+
145
+ > To enable LLM mode, see [Integration Guide §1](./docs/INTEGRATION_GUIDE.md#guardrail-mode-configuration).
146
+
147
+ ## Providers
148
+
149
+ | Provider | Description |
150
+ |:---|:---|
151
+ | **BYOC** (default) | Bring Your Own Card — encrypted vault credentials, local CDP injection. |
152
+ | **Stripe Issuing** | Real virtual cards via Stripe API. Requires `POP_STRIPE_KEY`. |
153
+ | **Lithic** | Multi-issuer adapter (Stripe Issuing / Lithic). |
154
+ | **Mock** | Test mode with generated card numbers for development. |
155
+
156
+ **Priority:** Stripe Issuing → BYOC Local → Mock.
157
+
158
+ ## Security
159
+
160
+ | Layer | Defense |
161
+ |---|---|
162
+ | **Context Isolation** | Card credentials never enter the agent's context window or logs |
163
+ | **Encrypted Vault** | AES-256-GCM with XOR-split salt and native scrypt key derivation (Rust) |
164
+ | **TOCTOU Guard** | Domain verified at the moment of CDP injection — blocks redirect attacks |
165
+ | **Repr Redaction** | Automatic masking (`****-4242`) in all MCP responses, logs, and tracebacks |
166
+
167
+ See [THREAT_MODEL.md](./docs/THREAT_MODEL.md) for the full STRIDE analysis and [COMPLIANCE_FAQ.md](./docs/COMPLIANCE_FAQ.md) for enterprise details.
168
+
169
+ ## Architecture
170
+
171
+ - **TypeScript** — MCP server, CDP injection engine, guardrails, CLI
172
+ - **Rust (napi-rs)** — Native security layer: XOR-split salt storage, scrypt key derivation
173
+ - **Node.js crypto** — AES-256-GCM vault encryption (OpenSSL binding)
174
+ - **Chrome DevTools Protocol** — Direct DOM injection via raw WebSocket
175
+
176
+ ## Documentation
177
+
178
+ - [Threat Model](docs/THREAT_MODEL.md) — STRIDE analysis, 5 security primitives, 10 attack scenarios
179
+ - [Guardrail Benchmark](docs/GUARDRAIL_BENCHMARK.md) — 95% accuracy across 20 test scenarios
180
+ - [Compliance FAQ](docs/COMPLIANCE_FAQ.md) — PCI DSS, SOC 2, GDPR details
181
+ - [Environment Reference](docs/ENV_REFERENCE.md) — All POP_* environment variables
182
+ - [Integration Guide](docs/INTEGRATION_GUIDE.md) — Setup for Claude Code, Node.js SDK, and browser agents
183
+ - [Categories Cookbook](docs/CATEGORIES_COOKBOOK.md) — POP_ALLOWED_CATEGORIES patterns and examples
184
+
33
185
  ## License
34
186
 
35
187
  MIT
@@ -0,0 +1,193 @@
1
+ :root {
2
+ --bg-color: #0d1117;
3
+ --card-bg: #161b22;
4
+ --text-primary: #c9d1d9;
5
+ --text-secondary: #8b949e;
6
+ --accent-green: #3fb950;
7
+ --warning-amber: #d29922;
8
+ --danger-red: #f85149;
9
+ --border-color: #30363d;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ body {
19
+ background-color: var(--bg-color);
20
+ color: var(--text-primary);
21
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
22
+ line-height: 1.6;
23
+ padding: 20px 0;
24
+ }
25
+
26
+ .container {
27
+ max-width: 1200px;
28
+ margin: 0 auto;
29
+ padding: 0 20px;
30
+ }
31
+
32
+ header {
33
+ margin-bottom: 40px;
34
+ }
35
+
36
+ header .container {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ border-bottom: 1px solid var(--border-color);
41
+ padding-bottom: 20px;
42
+ }
43
+
44
+ h1 {
45
+ font-size: 1.5rem;
46
+ font-weight: 700;
47
+ letter-spacing: 1px;
48
+ }
49
+
50
+ .subtitle {
51
+ color: var(--accent-green);
52
+ font-family: 'Courier New', Courier, monospace;
53
+ font-size: 0.9rem;
54
+ margin-left: 10px;
55
+ }
56
+
57
+ .metrics {
58
+ display: grid;
59
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
60
+ gap: 20px;
61
+ margin-bottom: 30px;
62
+ }
63
+
64
+ .card {
65
+ background-color: var(--card-bg);
66
+ border: 1px solid var(--border-color);
67
+ border-radius: 6px;
68
+ padding: 20px;
69
+ }
70
+
71
+ .card h3 {
72
+ color: var(--text-secondary);
73
+ font-size: 0.75rem;
74
+ letter-spacing: 1px;
75
+ margin-bottom: 10px;
76
+ text-transform: uppercase;
77
+ }
78
+
79
+ .value {
80
+ font-family: 'Courier New', Courier, monospace;
81
+ font-size: 2rem;
82
+ font-weight: 700;
83
+ }
84
+
85
+ .progress-bar {
86
+ background-color: var(--border-color);
87
+ height: 8px;
88
+ border-radius: 4px;
89
+ margin-top: 15px;
90
+ overflow: hidden;
91
+ }
92
+
93
+ .progress-fill {
94
+ height: 100%;
95
+ width: 0%;
96
+ background-color: var(--accent-green);
97
+ transition: width 0.5s ease;
98
+ }
99
+
100
+ .settings {
101
+ margin-bottom: 30px;
102
+ }
103
+
104
+ .input-group {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 15px;
108
+ margin-top: 10px;
109
+ }
110
+
111
+ label {
112
+ font-size: 0.85rem;
113
+ color: var(--text-secondary);
114
+ }
115
+
116
+ input[type="number"] {
117
+ background-color: var(--bg-color);
118
+ border: 1px solid var(--border-color);
119
+ color: var(--text-primary);
120
+ padding: 8px 12px;
121
+ border-radius: 4px;
122
+ width: 150px;
123
+ font-family: 'Courier New', Courier, monospace;
124
+ }
125
+
126
+ .btn {
127
+ background-color: transparent;
128
+ border: 1px solid var(--border-color);
129
+ color: var(--text-primary);
130
+ padding: 8px 16px;
131
+ border-radius: 6px;
132
+ cursor: pointer;
133
+ font-size: 0.85rem;
134
+ font-weight: 600;
135
+ transition: all 0.2s;
136
+ }
137
+
138
+ .btn:hover {
139
+ background-color: var(--card-bg);
140
+ border-color: var(--text-secondary);
141
+ }
142
+
143
+ .btn-small {
144
+ padding: 6px 12px;
145
+ }
146
+
147
+ .table-container {
148
+ overflow-x: auto;
149
+ margin-top: 15px;
150
+ }
151
+
152
+ table {
153
+ width: 100%;
154
+ border-collapse: collapse;
155
+ font-size: 0.85rem;
156
+ }
157
+
158
+ th {
159
+ text-align: left;
160
+ padding: 12px;
161
+ border-bottom: 1px solid var(--border-color);
162
+ color: var(--text-secondary);
163
+ text-transform: uppercase;
164
+ cursor: pointer;
165
+ }
166
+
167
+ th:hover {
168
+ color: var(--text-primary);
169
+ }
170
+
171
+ td {
172
+ padding: 12px;
173
+ border-bottom: 1px solid var(--border-color);
174
+ font-family: 'Courier New', Courier, monospace;
175
+ }
176
+
177
+ .seals, .rejected {
178
+ margin-bottom: 40px;
179
+ }
180
+
181
+ .rejected table {
182
+ border: 1px solid var(--danger-red);
183
+ }
184
+
185
+ .rejected td {
186
+ color: var(--danger-red);
187
+ }
188
+
189
+ @media (max-width: 768px) {
190
+ .metrics {
191
+ grid-template-columns: 1fr;
192
+ }
193
+ }
@@ -0,0 +1,135 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const todaySpendingEl = document.getElementById('today-spending');
3
+ const remainingBudgetEl = document.getElementById('remaining-budget');
4
+ const utilizationPctEl = document.getElementById('utilization-pct');
5
+ const utilizationFillEl = document.getElementById('utilization-fill');
6
+ const sealsBody = document.getElementById('seals-body');
7
+ const rejectedBody = document.getElementById('rejected-body');
8
+ const refreshBtn = document.getElementById('refresh-btn');
9
+ const maxBudgetInput = document.getElementById('max-daily-budget');
10
+ const saveSettingsBtn = document.getElementById('save-settings');
11
+
12
+ let sealsData = [];
13
+
14
+ const formatCurrency = (amount) => {
15
+ return new Intl.NumberFormat('en-US', {
16
+ style: 'currency',
17
+ currency: 'USD',
18
+ }).format(amount);
19
+ };
20
+
21
+ const fetchData = async () => {
22
+ try {
23
+ const [budgetRes, sealsRes, rejectedRes] = await Promise.all([
24
+ fetch('/api/budget/today'),
25
+ fetch('/api/seals'),
26
+ fetch('/api/seals?status=rejected')
27
+ ]);
28
+
29
+ const budget = await budgetRes.json();
30
+ sealsData = await sealsRes.json();
31
+ const rejected = await rejectedRes.json();
32
+
33
+ updateBudget(budget);
34
+ renderSeals(sealsData);
35
+ renderRejected(rejected);
36
+ } catch (error) {
37
+ console.error('Failed to fetch dashboard data:', error);
38
+ }
39
+ };
40
+
41
+ const updateBudget = (data) => {
42
+ const { spent, max, remaining } = data;
43
+ todaySpendingEl.textContent = formatCurrency(spent);
44
+ remainingBudgetEl.textContent = formatCurrency(remaining);
45
+
46
+ const utilization = max > 0 ? (spent / max) * 100 : 0;
47
+ utilizationPctEl.textContent = `${utilization.toFixed(1)}%`;
48
+ utilizationFillEl.style.width = `${Math.min(utilization, 100)}%`;
49
+
50
+ if (utilization >= 90) {
51
+ utilizationFillEl.style.backgroundColor = 'var(--danger-red)';
52
+ } else if (utilization >= 70) {
53
+ utilizationFillEl.style.backgroundColor = 'var(--warning-amber)';
54
+ } else {
55
+ utilizationFillEl.style.backgroundColor = 'var(--accent-green)';
56
+ }
57
+
58
+ maxBudgetInput.value = max;
59
+ };
60
+
61
+ const renderSeals = (seals) => {
62
+ sealsBody.innerHTML = '';
63
+ seals.forEach(seal => {
64
+ const row = document.createElement('tr');
65
+ row.innerHTML = `
66
+ <td>${seal.seal_id}</td>
67
+ <td>${formatCurrency(seal.amount)}</td>
68
+ <td>${seal.vendor}</td>
69
+ <td style="color: ${getStatusColor(seal.status)}">${seal.status}</td>
70
+ <td>${seal.masked_card || 'N/A'}</td>
71
+ <td>${new Date(seal.timestamp).toLocaleString()}</td>
72
+ `;
73
+ sealsBody.appendChild(row);
74
+ });
75
+ };
76
+
77
+ const renderRejected = (rejected) => {
78
+ rejectedBody.innerHTML = '';
79
+ rejected.forEach(seal => {
80
+ const row = document.createElement('tr');
81
+ row.innerHTML = `
82
+ <td>${seal.seal_id}</td>
83
+ <td>${formatCurrency(seal.amount)}</td>
84
+ <td>${seal.vendor}</td>
85
+ <td>${seal.status}</td>
86
+ <td>${new Date(seal.timestamp).toLocaleString()}</td>
87
+ `;
88
+ rejectedBody.appendChild(row);
89
+ });
90
+ };
91
+
92
+ const getStatusColor = (status) => {
93
+ switch (status.toLowerCase()) {
94
+ case 'issued': return 'var(--accent-green)';
95
+ case 'used': return 'var(--text-secondary)';
96
+ case 'rejected': return 'var(--danger-red)';
97
+ default: return 'var(--text-primary)';
98
+ }
99
+ };
100
+
101
+ const saveSettings = async () => {
102
+ const maxBudget = parseFloat(maxBudgetInput.value);
103
+ if (isNaN(maxBudget)) return;
104
+
105
+ try {
106
+ await fetch('/api/settings/max_daily_budget', {
107
+ method: 'PUT',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ value: maxBudget.toString() })
110
+ });
111
+ fetchData();
112
+ } catch (error) {
113
+ console.error('Failed to save settings:', error);
114
+ }
115
+ };
116
+
117
+ // Sorting logic
118
+ document.querySelectorAll('#seals-table th[data-sort]').forEach(th => {
119
+ th.addEventListener('click', () => {
120
+ const prop = th.dataset.sort;
121
+ const sorted = [...sealsData].sort((a, b) => {
122
+ if (a[prop] < b[prop]) return -1;
123
+ if (a[prop] > b[prop]) return 1;
124
+ return 0;
125
+ });
126
+ renderSeals(sorted);
127
+ });
128
+ });
129
+
130
+ refreshBtn.addEventListener('click', fetchData);
131
+ saveSettingsBtn.addEventListener('click', saveSettings);
132
+
133
+ // Initial load
134
+ fetchData();
135
+ });
@@ -0,0 +1,89 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>The Vault - Pop Pay Dashboard</title>
7
+ <link rel="stylesheet" href="dashboard.css">
8
+ </head>
9
+ <body>
10
+ <header>
11
+ <div class="container">
12
+ <h1>THE VAULT <span class="subtitle">// POP PAY TERMINAL</span></h1>
13
+ <button id="refresh-btn" class="btn">REFRESH_DATA</button>
14
+ </div>
15
+ </header>
16
+
17
+ <main class="container">
18
+ <section class="metrics">
19
+ <div class="card">
20
+ <h3>TODAY_SPENDING</h3>
21
+ <p id="today-spending" class="value">$0.00</p>
22
+ </div>
23
+ <div class="card">
24
+ <h3>REMAINING_BUDGET</h3>
25
+ <p id="remaining-budget" class="value">$0.00</p>
26
+ </div>
27
+ <div class="card">
28
+ <h3>UTILIZATION</h3>
29
+ <p id="utilization-pct" class="value">0%</p>
30
+ <div class="progress-bar">
31
+ <div id="utilization-fill" class="progress-fill"></div>
32
+ </div>
33
+ </div>
34
+ </section>
35
+
36
+ <section class="settings card">
37
+ <h3>BUDGET_SETTINGS</h3>
38
+ <div class="input-group">
39
+ <label for="max-daily-budget">MAX_DAILY_BUDGET ($):</label>
40
+ <input type="number" id="max-daily-budget" value="500" step="10">
41
+ <button id="save-settings" class="btn btn-small">UPDATE</button>
42
+ </div>
43
+ </section>
44
+
45
+ <section class="seals">
46
+ <h3>ISSUED_SEALS</h3>
47
+ <div class="table-container">
48
+ <table id="seals-table">
49
+ <thead>
50
+ <tr>
51
+ <th data-sort="seal_id">SEAL_ID</th>
52
+ <th data-sort="amount">AMOUNT</th>
53
+ <th data-sort="vendor">VENDOR</th>
54
+ <th data-sort="status">STATUS</th>
55
+ <th data-sort="masked_card">CARD_MASK</th>
56
+ <th data-sort="timestamp">TIMESTAMP</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody id="seals-body">
60
+ <!-- Data injected here -->
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+ </section>
65
+
66
+ <section class="rejected">
67
+ <h3>REJECTION_LOG</h3>
68
+ <div class="table-container">
69
+ <table id="rejected-table">
70
+ <thead>
71
+ <tr>
72
+ <th>SEAL_ID</th>
73
+ <th>AMOUNT</th>
74
+ <th>VENDOR</th>
75
+ <th>REASON</th>
76
+ <th>TIMESTAMP</th>
77
+ </tr>
78
+ </thead>
79
+ <tbody id="rejected-body">
80
+ <!-- Data injected here -->
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+ </section>
85
+ </main>
86
+
87
+ <script src="dashboard.js"></script>
88
+ </body>
89
+ </html>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli-dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-dashboard.d.ts","sourceRoot":"","sources":["../src/cli-dashboard.ts"],"names":[],"mappings":""}
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const dashboard_js_1 = require("./dashboard.js");
5
+ function parseArgs() {
6
+ const args = process.argv.slice(2);
7
+ const options = {
8
+ port: 3210,
9
+ dbPath: "pop_state.db"
10
+ };
11
+ for (let i = 0; i < args.length; i++) {
12
+ if (args[i] === "--port" && args[i + 1]) {
13
+ options.port = parseInt(args[i + 1], 10);
14
+ i++;
15
+ }
16
+ else if (args[i] === "--db" && args[i + 1]) {
17
+ options.dbPath = args[i + 1];
18
+ i++;
19
+ }
20
+ }
21
+ return options;
22
+ }
23
+ const options = parseArgs();
24
+ (0, dashboard_js_1.main)(options).catch(err => {
25
+ console.error("Failed to start dashboard:", err);
26
+ process.exit(1);
27
+ });
28
+ //# sourceMappingURL=cli-dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-dashboard.js","sourceRoot":"","sources":["../src/cli-dashboard.ts"],"names":[],"mappings":";;;AACA,iDAAsC;AAEtC,SAAS,SAAS;IAChB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG;QACd,IAAI,EAAE,IAAI;QACV,MAAM,EAAE,cAAc;KACvB,CAAC;IAEF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzC,CAAC,EAAE,CAAC;QACN,CAAC;aAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC7C,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC7B,CAAC,EAAE,CAAC;QACN,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,OAAO,GAAG,SAAS,EAAE,CAAC;AAC5B,IAAA,mBAAI,EAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;IACxB,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,GAAG,CAAC,CAAC;IACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import http from "node:http";
2
+ export interface DashboardOptions {
3
+ port: number;
4
+ dbPath: string;
5
+ }
6
+ export declare function main(options: DashboardOptions & {
7
+ skipOpen?: boolean;
8
+ }): Promise<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>>;
9
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAM7B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,IAAI,CAAC,OAAO,EAAE,gBAAgB,GAAG;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,iFA0H5E"}