@payclaw/badge 0.7.0 → 0.8.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 +68 -2
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +34 -3
- package/dist/api/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -12
- package/dist/index.js.map +1 -1
- package/dist/lib/device-auth.d.ts +30 -0
- package/dist/lib/device-auth.js +92 -0
- package/dist/lib/device-auth.js.map +1 -0
- package/dist/lib/parse-outcome.d.ts +5 -0
- package/dist/lib/parse-outcome.js +38 -0
- package/dist/lib/parse-outcome.js.map +1 -0
- package/dist/lib/parse-outcome.test.d.ts +1 -0
- package/dist/lib/parse-outcome.test.js +47 -0
- package/dist/lib/parse-outcome.test.js.map +1 -0
- package/dist/lib/report-badge-presented-handler.d.ts +7 -1
- package/dist/lib/report-badge-presented-handler.js +13 -2
- package/dist/lib/report-badge-presented-handler.js.map +1 -1
- package/dist/lib/report-badge.d.ts +3 -2
- package/dist/lib/report-badge.js +12 -8
- package/dist/lib/report-badge.js.map +1 -1
- package/dist/lib/report-badge.test.d.ts +1 -0
- package/dist/lib/report-badge.test.js +109 -0
- package/dist/lib/report-badge.test.js.map +1 -0
- package/dist/lib/storage.d.ts +17 -0
- package/dist/lib/storage.js +86 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/ucp-manifest.d.ts +32 -0
- package/dist/lib/ucp-manifest.js +117 -0
- package/dist/lib/ucp-manifest.js.map +1 -0
- package/dist/lib/ucp-manifest.test.d.ts +1 -0
- package/dist/lib/ucp-manifest.test.js +92 -0
- package/dist/lib/ucp-manifest.test.js.map +1 -0
- package/dist/sampling.d.ts +19 -1
- package/dist/sampling.js +74 -33
- package/dist/sampling.js.map +1 -1
- package/dist/sampling.test.d.ts +1 -0
- package/dist/sampling.test.js +150 -0
- package/dist/sampling.test.js.map +1 -0
- package/dist/tools/getAgentIdentity.d.ts +16 -2
- package/dist/tools/getAgentIdentity.js +179 -12
- package/dist/tools/getAgentIdentity.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/verify.d.ts +34 -0
- package/dist/verify.js +161 -0
- package/dist/verify.js.map +1 -0
- package/dist/verify.test.d.ts +1 -0
- package/dist/verify.test.js +177 -0
- package/dist/verify.test.js.map +1 -0
- package/package.json +10 -4
package/dist/sampling.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { getBaseUrl } from "./api/client.js";
|
|
2
|
+
import { parseResponse } from "./lib/parse-outcome.js";
|
|
3
|
+
import { getStoredConsentKey } from "./lib/storage.js";
|
|
1
4
|
const SAMPLING_DELAY_MS = 7000; // 7 seconds after identity_presented
|
|
2
5
|
const SAMPLING_TIMEOUT_MS = 15000; // 15 seconds to respond
|
|
3
|
-
const FAILURE_SIGNALS = [
|
|
4
|
-
"yes", "blocked", "denied", "failed", "403", "error",
|
|
5
|
-
"rejected", "banned", "forbidden", "captcha", "stopped",
|
|
6
|
-
];
|
|
7
6
|
// In-memory state — max 100 active trips
|
|
8
7
|
const activeTrips = new Map();
|
|
9
8
|
const MAX_TRIPS = 100;
|
|
@@ -14,12 +13,16 @@ let serverRef = null;
|
|
|
14
13
|
let samplingAvailable = false;
|
|
15
14
|
export function initSampling(server) {
|
|
16
15
|
serverRef = server;
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
16
|
+
// Extended Auth: only use sampling (agent confirmation prompt) when explicitly enabled.
|
|
17
|
+
// Otherwise, agent reports outcome via payclaw_reportBadgeOutcome.
|
|
18
|
+
const useExtendedAuth = process.env.PAYCLAW_EXTENDED_AUTH === "true" ||
|
|
19
|
+
process.env.PAYCLAW_EXTENDED_AUTH === "1";
|
|
20
|
+
samplingAvailable = useExtendedAuth; // Will catch errors on first attempt if enabled
|
|
20
21
|
if (!reaperStarted) {
|
|
21
22
|
reaperStarted = true;
|
|
22
|
-
|
|
23
|
+
if (process.env.VITEST !== "true") {
|
|
24
|
+
setInterval(() => reapStaleTrips(), REAPER_INTERVAL_MS);
|
|
25
|
+
}
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
export function onTripStarted(token, merchant) {
|
|
@@ -65,9 +68,6 @@ export function onIdentityPresented(token, merchant) {
|
|
|
65
68
|
clearTimeout(t.samplingTimer);
|
|
66
69
|
t.samplingTimer = setTimeout(() => sampleAgent(token, merchant), SAMPLING_DELAY_MS);
|
|
67
70
|
}
|
|
68
|
-
export function reportOutcomeFromAgent(token, merchant, outcome) {
|
|
69
|
-
resolveTrip(token, outcome, "agent_reported");
|
|
70
|
-
}
|
|
71
71
|
async function sampleAgent(token, merchant) {
|
|
72
72
|
const trip = activeTrips.get(token);
|
|
73
73
|
if (!trip || trip.outcome)
|
|
@@ -130,22 +130,6 @@ async function sampleAgent(token, merchant) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
function parseResponse(text) {
|
|
134
|
-
if (!text || text.trim().length === 0)
|
|
135
|
-
return "inconclusive";
|
|
136
|
-
const lower = text.toLowerCase().trim();
|
|
137
|
-
// Check for denial signals
|
|
138
|
-
if (FAILURE_SIGNALS.some((s) => lower.includes(s))) {
|
|
139
|
-
// But "no" alone means "no, I was not denied" = accepted
|
|
140
|
-
if (lower === "no" || lower === "no." || lower === "no,")
|
|
141
|
-
return "accepted";
|
|
142
|
-
return "denied";
|
|
143
|
-
}
|
|
144
|
-
// "no" variants = not denied = accepted
|
|
145
|
-
if (lower.startsWith("no"))
|
|
146
|
-
return "accepted";
|
|
147
|
-
return "inconclusive";
|
|
148
|
-
}
|
|
149
133
|
function resolveTrip(token, outcome, detail) {
|
|
150
134
|
const trip = activeTrips.get(token);
|
|
151
135
|
if (!trip)
|
|
@@ -161,15 +145,15 @@ function resolveTrip(token, outcome, detail) {
|
|
|
161
145
|
activeTrips.delete(token);
|
|
162
146
|
}
|
|
163
147
|
async function reportOutcome(token, outcome, merchant, detail) {
|
|
164
|
-
const apiUrl =
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
148
|
+
const apiUrl = getBaseUrl();
|
|
149
|
+
const key = getStoredConsentKey();
|
|
150
|
+
if (!key)
|
|
167
151
|
return;
|
|
168
152
|
const eventType = outcome === "denied" ? "trip_failure" : "trip_success";
|
|
169
153
|
const res = await fetch(`${apiUrl}/api/badge/report`, {
|
|
170
154
|
method: "POST",
|
|
171
155
|
headers: {
|
|
172
|
-
Authorization: `Bearer ${
|
|
156
|
+
Authorization: `Bearer ${key}`,
|
|
173
157
|
"Content-Type": "application/json",
|
|
174
158
|
},
|
|
175
159
|
body: JSON.stringify({
|
|
@@ -187,25 +171,82 @@ async function reportOutcome(token, outcome, merchant, detail) {
|
|
|
187
171
|
}
|
|
188
172
|
function reapStaleTrips() {
|
|
189
173
|
const now = Date.now();
|
|
174
|
+
let reaped = 0;
|
|
190
175
|
for (const [token, trip] of activeTrips) {
|
|
191
176
|
if (now - trip.startedAt > STALE_TRIP_MS) {
|
|
177
|
+
const ageMin = Math.round((now - trip.startedAt) / 60000);
|
|
192
178
|
if (trip.presented && !trip.outcome) {
|
|
179
|
+
process.stderr.write(`[PayClaw] Reaped stale trip: ${token.slice(0, 10)}** (${trip.merchant.slice(0, 64)}, age: ${ageMin}m)\n`);
|
|
193
180
|
resolveTrip(token, "inconclusive", "stale_trip_reaped");
|
|
181
|
+
reaped++;
|
|
194
182
|
}
|
|
195
183
|
else {
|
|
196
184
|
activeTrips.delete(token);
|
|
185
|
+
reaped++;
|
|
197
186
|
}
|
|
198
187
|
}
|
|
199
188
|
}
|
|
189
|
+
if (activeTrips.size > 0 || reaped > 0) {
|
|
190
|
+
process.stderr.write(`[PayClaw] Active trips: ${activeTrips.size} | Reaped: ${reaped}\n`);
|
|
191
|
+
}
|
|
200
192
|
}
|
|
201
193
|
// Called when MCP client disconnects
|
|
202
194
|
export function onServerClose() {
|
|
203
195
|
for (const [token, trip] of activeTrips) {
|
|
204
196
|
if (trip.presented && !trip.outcome) {
|
|
205
|
-
// Agent
|
|
206
|
-
resolveTrip(token, "
|
|
197
|
+
// Agent disconnected — outcome unknown
|
|
198
|
+
resolveTrip(token, "inconclusive", "server_close");
|
|
207
199
|
}
|
|
208
200
|
}
|
|
209
201
|
activeTrips.clear();
|
|
210
202
|
}
|
|
203
|
+
/** Test-only: reset state between tests. No-op when VITEST not set. */
|
|
204
|
+
export function resetSamplingState() {
|
|
205
|
+
if (process.env.VITEST !== "true")
|
|
206
|
+
return;
|
|
207
|
+
for (const trip of activeTrips.values()) {
|
|
208
|
+
if (trip.samplingTimer)
|
|
209
|
+
clearTimeout(trip.samplingTimer);
|
|
210
|
+
}
|
|
211
|
+
activeTrips.clear();
|
|
212
|
+
serverRef = null;
|
|
213
|
+
samplingAvailable = true;
|
|
214
|
+
reaperStarted = false;
|
|
215
|
+
}
|
|
216
|
+
/** Test-only: get trip for assertions. Returns undefined when VITEST not set. */
|
|
217
|
+
export function getActiveTrip(token) {
|
|
218
|
+
if (process.env.VITEST !== "true")
|
|
219
|
+
return undefined;
|
|
220
|
+
return activeTrips.get(token);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Report outcome from agent (payclaw_reportBadgeOutcome tool).
|
|
224
|
+
* Agent-only path — no sampling prompt. Resolves trip and POSTs to API.
|
|
225
|
+
* When token not in activeTrips (e.g. after restart), looks up by merchant or POSTs directly.
|
|
226
|
+
*/
|
|
227
|
+
export function reportOutcomeFromAgent(token, merchant, outcome) {
|
|
228
|
+
if (activeTrips.has(token)) {
|
|
229
|
+
resolveTrip(token, outcome, "agent_reported");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Token may be from before restart — try to find a unique trip by merchant
|
|
233
|
+
let matchToken = null;
|
|
234
|
+
let matchCount = 0;
|
|
235
|
+
for (const [t, trip] of activeTrips) {
|
|
236
|
+
if (trip.merchant === merchant && trip.presented && !trip.outcome) {
|
|
237
|
+
matchToken = t;
|
|
238
|
+
matchCount++;
|
|
239
|
+
if (matchCount > 1)
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (matchCount === 1 && matchToken) {
|
|
244
|
+
resolveTrip(matchToken, outcome, "agent_reported");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// No matching trip — still report to API so outcome is recorded
|
|
248
|
+
reportOutcome(token, outcome, merchant, "agent_reported").catch((err) => {
|
|
249
|
+
process.stderr.write(`[BADGE] Failed to report outcome: ${err}\n`);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
211
252
|
//# sourceMappingURL=sampling.js.map
|
package/dist/sampling.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sampling.js","sourceRoot":"","sources":["../src/sampling.ts"],"names":[],"mappings":"AAEA,
|
|
1
|
+
{"version":3,"file":"sampling.js","sourceRoot":"","sources":["../src/sampling.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach, } from "vitest";
|
|
2
|
+
import { onTripStarted, onIdentityPresented, onServerClose, resetSamplingState, getActiveTrip, reportOutcomeFromAgent, } from "./sampling.js";
|
|
3
|
+
import * as storage from "./lib/storage.js";
|
|
4
|
+
vi.mock("./lib/storage.js", () => ({
|
|
5
|
+
getStoredConsentKey: vi.fn(),
|
|
6
|
+
storeConsentKey: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("./api/client.js", () => ({
|
|
9
|
+
getBaseUrl: vi.fn().mockReturnValue("https://payclaw.io"),
|
|
10
|
+
}));
|
|
11
|
+
describe("sampling — multi-merchant trip lifecycle", () => {
|
|
12
|
+
let originalVitest;
|
|
13
|
+
const mockFetch = vi.fn();
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
originalVitest = process.env.VITEST;
|
|
16
|
+
process.env.VITEST = "true";
|
|
17
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
18
|
+
mockFetch.mockResolvedValue({ ok: true });
|
|
19
|
+
vi.mocked(storage.getStoredConsentKey).mockReturnValue("pk_test_xxx");
|
|
20
|
+
resetSamplingState();
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
resetSamplingState();
|
|
25
|
+
vi.unstubAllGlobals();
|
|
26
|
+
if (originalVitest !== undefined) {
|
|
27
|
+
process.env.VITEST = originalVitest;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
delete process.env.VITEST;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it("starting trip B resolves presented trip A as agent_moved_to_new_merchant", () => {
|
|
34
|
+
onTripStarted("tok_a", "amazon.com");
|
|
35
|
+
onIdentityPresented("tok_a", "amazon.com");
|
|
36
|
+
// Trip A is active and presented
|
|
37
|
+
expect(getActiveTrip("tok_a")).toBeDefined();
|
|
38
|
+
expect(getActiveTrip("tok_a").presented).toBe(true);
|
|
39
|
+
// Start trip B at a different merchant
|
|
40
|
+
onTripStarted("tok_b", "target.com");
|
|
41
|
+
// Trip A should be resolved and evicted (agent moved on)
|
|
42
|
+
expect(getActiveTrip("tok_a")).toBeUndefined();
|
|
43
|
+
// Trip B should be active
|
|
44
|
+
const tripB = getActiveTrip("tok_b");
|
|
45
|
+
expect(tripB).toBeDefined();
|
|
46
|
+
expect(tripB.merchant).toBe("target.com");
|
|
47
|
+
expect(tripB.presented).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it("trip B can be presented and resolved after trip A is auto-resolved", () => {
|
|
50
|
+
onTripStarted("tok_a", "amazon.com");
|
|
51
|
+
onIdentityPresented("tok_a", "amazon.com");
|
|
52
|
+
onTripStarted("tok_b", "target.com");
|
|
53
|
+
onIdentityPresented("tok_b", "target.com");
|
|
54
|
+
const tripB = getActiveTrip("tok_b");
|
|
55
|
+
expect(tripB).toBeDefined();
|
|
56
|
+
expect(tripB.presented).toBe(true);
|
|
57
|
+
// Resolve trip B via agent report
|
|
58
|
+
reportOutcomeFromAgent("tok_b", "target.com", "accepted");
|
|
59
|
+
expect(getActiveTrip("tok_b")).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
it("non-presented trip A is NOT resolved when trip B starts", () => {
|
|
62
|
+
onTripStarted("tok_a", "amazon.com");
|
|
63
|
+
// Never call onIdentityPresented for tok_a
|
|
64
|
+
onTripStarted("tok_b", "target.com");
|
|
65
|
+
// Trip A should still exist (not presented, so agent_moved logic skips it)
|
|
66
|
+
expect(getActiveTrip("tok_a")).toBeDefined();
|
|
67
|
+
expect(getActiveTrip("tok_b")).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
it("same-merchant trip restart does not resolve existing trip", () => {
|
|
70
|
+
onTripStarted("tok_a", "amazon.com");
|
|
71
|
+
onIdentityPresented("tok_a", "amazon.com");
|
|
72
|
+
// Start another trip at SAME merchant — should NOT resolve trip A
|
|
73
|
+
onTripStarted("tok_b", "amazon.com");
|
|
74
|
+
// Trip A still active (same merchant = not "moved to new merchant")
|
|
75
|
+
expect(getActiveTrip("tok_a")).toBeDefined();
|
|
76
|
+
expect(getActiveTrip("tok_b")).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
it("three-merchant chain resolves each previous trip correctly", () => {
|
|
79
|
+
onTripStarted("tok_1", "amazon.com");
|
|
80
|
+
onIdentityPresented("tok_1", "amazon.com");
|
|
81
|
+
onTripStarted("tok_2", "target.com");
|
|
82
|
+
expect(getActiveTrip("tok_1")).toBeUndefined(); // resolved
|
|
83
|
+
onIdentityPresented("tok_2", "target.com");
|
|
84
|
+
onTripStarted("tok_3", "walmart.com");
|
|
85
|
+
expect(getActiveTrip("tok_2")).toBeUndefined(); // resolved
|
|
86
|
+
onIdentityPresented("tok_3", "walmart.com");
|
|
87
|
+
// Only tok_3 should remain
|
|
88
|
+
expect(getActiveTrip("tok_3")).toBeDefined();
|
|
89
|
+
expect(getActiveTrip("tok_3").merchant).toBe("walmart.com");
|
|
90
|
+
});
|
|
91
|
+
it("reportOutcomeFromAgent falls back to merchant search when token unknown", () => {
|
|
92
|
+
onTripStarted("tok_a", "amazon.com");
|
|
93
|
+
onIdentityPresented("tok_a", "amazon.com");
|
|
94
|
+
// Report with a different token but matching merchant
|
|
95
|
+
reportOutcomeFromAgent("unknown_tok", "amazon.com", "accepted");
|
|
96
|
+
// Trip should be resolved via merchant search fallback
|
|
97
|
+
expect(getActiveTrip("tok_a")).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
it("reportOutcomeFromAgent with no matching trip still POSTs to API", () => {
|
|
100
|
+
mockFetch.mockClear();
|
|
101
|
+
reportOutcomeFromAgent("orphan_tok", "orphan.com", "denied");
|
|
102
|
+
// Should have called fetch to POST directly
|
|
103
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
104
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
105
|
+
expect(url).toContain("/api/badge/report");
|
|
106
|
+
expect(JSON.parse(opts.body)).toMatchObject({
|
|
107
|
+
verification_token: "orphan_tok",
|
|
108
|
+
merchant: "orphan.com",
|
|
109
|
+
outcome: "denied",
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it("onServerClose resolves all presented trips as inconclusive", () => {
|
|
113
|
+
onTripStarted("tok_a", "amazon.com");
|
|
114
|
+
onIdentityPresented("tok_a", "amazon.com");
|
|
115
|
+
onTripStarted("tok_b", "amazon.com"); // Same merchant — no auto-resolve
|
|
116
|
+
onIdentityPresented("tok_b", "amazon.com");
|
|
117
|
+
onServerClose();
|
|
118
|
+
expect(getActiveTrip("tok_a")).toBeUndefined();
|
|
119
|
+
expect(getActiveTrip("tok_b")).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
it("reportOutcomeFromAgent with ambiguous merchant match falls through to API POST", () => {
|
|
122
|
+
mockFetch.mockClear();
|
|
123
|
+
// Two trips at same merchant, both presented
|
|
124
|
+
onTripStarted("tok_a", "amazon.com");
|
|
125
|
+
onIdentityPresented("tok_a", "amazon.com");
|
|
126
|
+
onTripStarted("tok_b", "amazon.com");
|
|
127
|
+
onIdentityPresented("tok_b", "amazon.com");
|
|
128
|
+
mockFetch.mockClear();
|
|
129
|
+
reportOutcomeFromAgent("unknown_tok", "amazon.com", "accepted");
|
|
130
|
+
// Should NOT resolve either trip (ambiguous) — falls through to direct POST
|
|
131
|
+
expect(getActiveTrip("tok_a")).toBeDefined();
|
|
132
|
+
expect(getActiveTrip("tok_b")).toBeDefined();
|
|
133
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(String(mockFetch.mock.calls[0][0])).toContain("/api/badge/report");
|
|
135
|
+
});
|
|
136
|
+
it("API report includes correct event_type for outcome", () => {
|
|
137
|
+
mockFetch.mockClear();
|
|
138
|
+
onTripStarted("tok_a", "amazon.com");
|
|
139
|
+
onIdentityPresented("tok_a", "amazon.com");
|
|
140
|
+
// Move to new merchant — resolves trip A as "accepted"
|
|
141
|
+
onTripStarted("tok_b", "target.com");
|
|
142
|
+
// Check the fetch call for trip A resolution
|
|
143
|
+
const reportCalls = mockFetch.mock.calls.filter((c) => String(c[0]).includes("/api/badge/report"));
|
|
144
|
+
expect(reportCalls.length).toBeGreaterThanOrEqual(1);
|
|
145
|
+
const body = JSON.parse(reportCalls[0][1].body);
|
|
146
|
+
expect(body.event_type).toBe("trip_success");
|
|
147
|
+
expect(body.detail).toBe("agent_moved_to_new_merchant");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
//# sourceMappingURL=sampling.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampling.test.js","sourceRoot":"","sources":["../src/sampling.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,10 +12,24 @@ export interface IdentityResult {
|
|
|
12
12
|
merchant?: string;
|
|
13
13
|
instructions?: string;
|
|
14
14
|
message?: string;
|
|
15
|
+
/** Internal: activation flow — agent should display this to user */
|
|
16
|
+
activation_required?: boolean;
|
|
17
|
+
/** UCP: merchant supports io.payclaw.common.identity */
|
|
18
|
+
ucpCapable?: boolean;
|
|
19
|
+
/** UCP: merchant requires PayClaw credential */
|
|
20
|
+
requiredByMerchant?: boolean;
|
|
21
|
+
/** UCP: checkout patch to merge into checkout payload */
|
|
22
|
+
checkoutPatch?: Record<string, unknown>;
|
|
23
|
+
/** UCP: warning when version mismatch etc. */
|
|
24
|
+
ucpWarning?: string;
|
|
15
25
|
}
|
|
16
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Get agent identity token — Badge by PayClaw.
|
|
28
|
+
* When no consent key exists: initiates device flow, returns activation instructions,
|
|
29
|
+
* polls in background. On approval, stores key. Next call uses stored key.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getAgentIdentity(merchant?: string, merchantUrl?: string): Promise<IdentityResult>;
|
|
17
32
|
/**
|
|
18
33
|
* Format identity result as human-readable text for CLI/agent display.
|
|
19
|
-
* Claude reads this and relays key info naturally to the user.
|
|
20
34
|
*/
|
|
21
35
|
export declare function formatIdentityResponse(r: IdentityResult): string;
|
|
@@ -1,18 +1,107 @@
|
|
|
1
|
+
// Canonical: badge-server | Synced: PRD-3 | mcp-server syncs from here
|
|
1
2
|
import * as api from "../api/client.js";
|
|
3
|
+
import { getStoredConsentKey } from "../lib/storage.js";
|
|
4
|
+
import { initiateDeviceAuth, pollForApproval } from "../lib/device-auth.js";
|
|
5
|
+
import { fetchUCPManifest, findPayClawCapability, isVersionCompatible } from "../lib/ucp-manifest.js";
|
|
2
6
|
const MOCK_TOKEN_PREFIX = "pc_v1_sand";
|
|
7
|
+
/** Must match the kid in the JWKS published at payclaw.io/.well-known/ucp (BUILD 3 / PRD-1) */
|
|
8
|
+
const PAYCLAW_KID = "payclaw-badge-v1";
|
|
3
9
|
function getMockDisclosure(scope = "BROWSE") {
|
|
4
10
|
return `This agent is using PayClaw Badge: Agent Intent for Ecommerce. The principal user token is a SHA-256 starting ${MOCK_TOKEN_PREFIX}***. Intent has been expressly user-authorized for this session for [${scope}]. For inquiries, please message agent_identity@payclaw.io`;
|
|
5
11
|
}
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
/** Build disclosure from OAuth token prefix (matches app disclosure format). */
|
|
13
|
+
function getDisclosureFromToken(token, scope = "BROWSE") {
|
|
14
|
+
const prefix = token.slice(0, 11);
|
|
15
|
+
return `This agent is using PayClaw Badge: Agent Intent for Ecommerce. The principal user token is a SHA-256 starting ${prefix}***. Intent has been expressly user-authorized for this session for [${scope}]. For inquiries, please message agent_identity@payclaw.io`;
|
|
16
|
+
}
|
|
17
|
+
/** Build identity result from OAuth token (when API doesn't accept OAuth Bearer yet). */
|
|
18
|
+
function identityFromOAuthToken(token, _assuranceLevel, merchant, assumeVerified = true) {
|
|
19
|
+
return {
|
|
20
|
+
product_name: "PayClaw Badge",
|
|
21
|
+
status: assumeVerified ? "active" : "pending",
|
|
22
|
+
agent_disclosure: getDisclosureFromToken(token),
|
|
23
|
+
verification_token: token,
|
|
24
|
+
trust_url: "https://payclaw.io/trust",
|
|
25
|
+
contact: "agent_identity@payclaw.io",
|
|
26
|
+
principal_verified: assumeVerified,
|
|
27
|
+
mfa_confirmed: false,
|
|
28
|
+
spend_available: false,
|
|
29
|
+
spend_cta: "For agent payments, use @payclaw/mcp-server — payclaw.io/docs",
|
|
30
|
+
merchant,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let pendingActivation = null;
|
|
34
|
+
/**
|
|
35
|
+
* Get agent identity token — Badge by PayClaw.
|
|
36
|
+
* When no consent key exists: initiates device flow, returns activation instructions,
|
|
37
|
+
* polls in background. On approval, stores key. Next call uses stored key.
|
|
38
|
+
*/
|
|
39
|
+
export async function getAgentIdentity(merchant, merchantUrl) {
|
|
40
|
+
const consentKey = getStoredConsentKey();
|
|
41
|
+
let result;
|
|
42
|
+
// Backward compat: PAYCLAW_API_KEY set → use it, device flow never triggers
|
|
43
|
+
if (consentKey && process.env.PAYCLAW_API_KEY) {
|
|
44
|
+
result = await callWithKey(consentKey, merchant);
|
|
45
|
+
}
|
|
46
|
+
else if (!consentKey) {
|
|
47
|
+
// No key: initiate device flow (reuse pending to avoid duplicate pollers)
|
|
48
|
+
if (pendingActivation)
|
|
49
|
+
return pendingActivation;
|
|
50
|
+
const p = startActivationFlow(merchant);
|
|
51
|
+
pendingActivation = p;
|
|
52
|
+
try {
|
|
53
|
+
result = await p;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
pendingActivation = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Key from file/memory (OAuth token from device flow)
|
|
61
|
+
result = await callWithOAuthToken(consentKey, merchant);
|
|
62
|
+
}
|
|
63
|
+
// UCP enrichment: check merchant manifest when merchantUrl provided
|
|
64
|
+
// Skip for mock/sandbox tokens — don't generate checkoutPatch for non-real credentials
|
|
65
|
+
if (merchantUrl &&
|
|
66
|
+
result.verification_token &&
|
|
67
|
+
!result.activation_required &&
|
|
68
|
+
!result.verification_token.startsWith(MOCK_TOKEN_PREFIX)) {
|
|
69
|
+
result = await enrichWithUCP(result, merchantUrl);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
async function enrichWithUCP(result, merchantUrl) {
|
|
74
|
+
const manifest = await fetchUCPManifest(merchantUrl);
|
|
75
|
+
if (!manifest) {
|
|
76
|
+
return { ...result, ucpCapable: false };
|
|
77
|
+
}
|
|
78
|
+
const capability = findPayClawCapability(manifest);
|
|
79
|
+
if (!capability) {
|
|
80
|
+
return { ...result, ucpCapable: false };
|
|
81
|
+
}
|
|
82
|
+
if (!isVersionCompatible(capability.version)) {
|
|
8
83
|
return {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
84
|
+
...result,
|
|
85
|
+
ucpCapable: false,
|
|
86
|
+
ucpWarning: `version mismatch: merchant declares ${capability.version}`,
|
|
12
87
|
};
|
|
13
88
|
}
|
|
89
|
+
const checkoutPatch = {
|
|
90
|
+
"io.payclaw.common.identity": {
|
|
91
|
+
token: result.verification_token,
|
|
92
|
+
kid: PAYCLAW_KID,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
...result,
|
|
97
|
+
ucpCapable: true,
|
|
98
|
+
requiredByMerchant: capability.required,
|
|
99
|
+
checkoutPatch,
|
|
100
|
+
instructions: "Merge checkoutPatch into your checkout payload, then call payclaw_reportBadgePresented with the merchantUrl and token.",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function callWithKey(apiKey, merchant) {
|
|
14
104
|
if (!api.isApiMode()) {
|
|
15
|
-
// Mock mode — return sandbox identity for local testing
|
|
16
105
|
return {
|
|
17
106
|
product_name: "PayClaw Badge",
|
|
18
107
|
status: "active",
|
|
@@ -42,25 +131,103 @@ export async function getAgentIdentity(merchant) {
|
|
|
42
131
|
};
|
|
43
132
|
}
|
|
44
133
|
}
|
|
134
|
+
async function callWithOAuthToken(token, merchant) {
|
|
135
|
+
if (!api.isApiMode()) {
|
|
136
|
+
return identityFromOAuthToken(token, undefined, merchant);
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const result = await api.getAgentIdentityWithToken(api.getBaseUrl(), token, merchant);
|
|
140
|
+
return {
|
|
141
|
+
product_name: "PayClaw Badge",
|
|
142
|
+
status: "active",
|
|
143
|
+
merchant: merchant || undefined,
|
|
144
|
+
...result,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// API call failed — do not mark as verified when falling back to local identity
|
|
149
|
+
return identityFromOAuthToken(token, undefined, merchant, false);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function startActivationFlow(merchant) {
|
|
153
|
+
try {
|
|
154
|
+
const deviceAuth = await initiateDeviceAuth();
|
|
155
|
+
const message = [
|
|
156
|
+
"[PayClaw MCP Server initializing...]",
|
|
157
|
+
"",
|
|
158
|
+
"🛡️ Merchants block anonymous bots. PayClaw proves your agent is authorized.",
|
|
159
|
+
"🔗 To issue your agent's Consent Key, we need your approval.",
|
|
160
|
+
"",
|
|
161
|
+
`👉 Go to: ${deviceAuth.verification_uri}`,
|
|
162
|
+
`🔑 Enter code: ${deviceAuth.user_code}`,
|
|
163
|
+
"",
|
|
164
|
+
"[⏳ Waiting for your approval...]",
|
|
165
|
+
].join("\n");
|
|
166
|
+
// Start polling in background — do not await
|
|
167
|
+
pollForApproval(deviceAuth.device_code, deviceAuth.interval, deviceAuth.expires_in, () => {
|
|
168
|
+
process.stderr.write([
|
|
169
|
+
"",
|
|
170
|
+
"[✅ Approval received]",
|
|
171
|
+
"",
|
|
172
|
+
"🎉 Consent Key generated and stored securely.",
|
|
173
|
+
"🐾 Avatar assigned: Starter Ghost (0 trips)",
|
|
174
|
+
"🔐 Your agent is now an authorized actor.",
|
|
175
|
+
"",
|
|
176
|
+
"Ready to shop. Agents are not bots.",
|
|
177
|
+
"",
|
|
178
|
+
].join("\n"));
|
|
179
|
+
}).catch(() => {
|
|
180
|
+
// Poll failed (expired, etc.) — user will need to retry
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
product_name: "PayClaw Badge",
|
|
184
|
+
status: "activation_required",
|
|
185
|
+
activation_required: true,
|
|
186
|
+
message,
|
|
187
|
+
merchant: merchant || undefined,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
return {
|
|
192
|
+
product_name: "PayClaw Badge",
|
|
193
|
+
status: "error",
|
|
194
|
+
message: err instanceof Error ? err.message : String(err),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
45
198
|
/**
|
|
46
199
|
* Format identity result as human-readable text for CLI/agent display.
|
|
47
|
-
* Claude reads this and relays key info naturally to the user.
|
|
48
200
|
*/
|
|
49
201
|
export function formatIdentityResponse(r) {
|
|
202
|
+
if (r.activation_required && r.message) {
|
|
203
|
+
return r.message;
|
|
204
|
+
}
|
|
50
205
|
if (r.status === "error") {
|
|
51
206
|
return `✗ BADGE ERROR\n\n ${r.message}`;
|
|
52
207
|
}
|
|
53
208
|
const lines = [
|
|
54
209
|
`✓ DECLARED — Your agent is now an authorized actor`,
|
|
55
210
|
``,
|
|
56
|
-
` Token: ${r.verification_token ? r.verification_token.slice(0, 10) +
|
|
57
|
-
` Principal: ${r.principal_verified ?
|
|
211
|
+
` Token: ${r.verification_token ? r.verification_token.slice(0, 10) + "**" : "N/A"}`,
|
|
212
|
+
` Principal: ${r.principal_verified ? "Verified ✓" : "Unverified"}`,
|
|
58
213
|
` Scope: [BROWSE]`,
|
|
59
214
|
];
|
|
60
215
|
if (r.merchant) {
|
|
61
216
|
lines.push(` Merchant: ${r.merchant}`);
|
|
62
217
|
}
|
|
63
|
-
lines.push(` Status: ACTIVE`, ` Trust: ${r.trust_url ||
|
|
218
|
+
lines.push(` Status: ACTIVE`, ` Trust: ${r.trust_url || "https://payclaw.io/trust"}`, ``, ` Disclosure (present to merchants):`, ` "${r.agent_disclosure}"`);
|
|
219
|
+
if (r.ucpCapable) {
|
|
220
|
+
lines.push(``, ` UCP: Supported`, ` Required: ${r.requiredByMerchant ? "Yes" : "No"}`);
|
|
221
|
+
if (r.instructions) {
|
|
222
|
+
lines.push(` Action: ${r.instructions}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (r.ucpCapable === false) {
|
|
226
|
+
lines.push(``, ` UCP: Not supported`);
|
|
227
|
+
if (r.ucpWarning) {
|
|
228
|
+
lines.push(` Warning: ${r.ucpWarning}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
64
231
|
if (r.spend_available) {
|
|
65
232
|
lines.push(``, ` 💳 Spend is available — call payclaw_getCard when ready to pay.`);
|
|
66
233
|
}
|
|
@@ -68,8 +235,8 @@ export function formatIdentityResponse(r) {
|
|
|
68
235
|
lines.push(``, ` ℹ️ ${r.spend_cta}`);
|
|
69
236
|
}
|
|
70
237
|
else {
|
|
71
|
-
lines.push(``, ` ℹ️ Identity only.
|
|
238
|
+
lines.push(``, ` ℹ️ Identity only. For agent payments, use @payclaw/mcp-server — payclaw.io/docs`);
|
|
72
239
|
}
|
|
73
|
-
return lines.join(
|
|
240
|
+
return lines.join("\n");
|
|
74
241
|
}
|
|
75
242
|
//# sourceMappingURL=getAgentIdentity.js.map
|