@reclaimprotocol/agent 0.1.25 → 0.1.26
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 +2 -0
- package/lib/old/client.d.ts +83 -0
- package/lib/old/client.js +46 -0
- package/lib/old/tools/build.js +2 -0
- package/lib/old/tools/session-logs.d.ts +3 -0
- package/lib/old/tools/session-logs.js +310 -0
- package/package.json +2 -2
- package/skill/SKILL.old.md +28 -2
- package/skill/old-mode.md +4 -2
- package/src/old/client.ts +128 -0
- package/src/old/tools/build.ts +2 -0
- package/src/old/tools/session-logs.ts +395 -0
package/README.md
CHANGED
|
@@ -113,6 +113,8 @@ The MCP server targets one of two backends, selected by `USE_OLD_DEVTOOLS`:
|
|
|
113
113
|
- `reclaim_authenticate` — by default opens the dashboard in Chrome via CDP, waits for sign-in, reads the Firebase token from page storage, and closes the tab (or pass a `token` / `ethAddress` to skip the browser).
|
|
114
114
|
- `create_provider_version_from_capture` — registers a new provider, or adds a version to an existing one when given a `providerId`.
|
|
115
115
|
- `get_me_providers` — list the providers you own (the only provider-listing tool in this mode).
|
|
116
|
+
- `session_analytics_logs` — the milestone events for a verification session, which show how far it got.
|
|
117
|
+
- `session_logs` — the log entries the in-app SDK emitted during that session, each with its event type, log level, and logger name. Filter by event type, level, or substring, page with `limit` and `offset`, or set `saveTo` to write every matching entry to an NDJSON file. The backend searches only the last 3 days unless you pass `startTime` and `endTime`, and it deletes entries after 30 days.
|
|
116
118
|
- **Builder (`USE_OLD_DEVTOOLS=false`).** Registers the auto-generated HTTP-API tools plus the authentication/credential tools (`authenticate` device-pairing login, `issue_credentials` / `import_credentials` to set up the local eth proof-owner key, `resolve_owner_key`) and a verification-result helper (`get_verification_result`).
|
|
117
119
|
|
|
118
120
|
Relevant env vars: `RECLAIM_OLD_API_URL` (default `https://devapi.reclaimprotocol.org`), `RECLAIM_OLD_API_TOKEN` / `RECLAIM_OLD_ETH_UID` (headless identity), `RECLAIM_OLD_LOGIN_URL` (default `https://dev.reclaimprotocol.org`); builder mode uses `RECLAIM_API_URL` / `RECLAIM_API_TOKEN`.
|
package/lib/old/client.d.ts
CHANGED
|
@@ -94,6 +94,70 @@ export interface LinkNonce {
|
|
|
94
94
|
nonce: string;
|
|
95
95
|
expiresInSeconds: number;
|
|
96
96
|
}
|
|
97
|
+
/** Log-level filter accepted by the devtools backend's session-logs route.
|
|
98
|
+
* It validates against this exact list and rejects anything else with a 400,
|
|
99
|
+
* including the comma-separated lists the log-stream service itself would
|
|
100
|
+
* accept. Each value is hierarchical at the log-stream end: `fine` means
|
|
101
|
+
* FINE+CONFIG+INFO+WARNING+SEVERE, `info` means INFO+WARNING+SEVERE, and so
|
|
102
|
+
* on. `finer`/`finest` (the PII tiers) are not reachable through this route
|
|
103
|
+
* at all. */
|
|
104
|
+
export type SessionLogLevel = 'fine' | 'config' | 'info' | 'warning' | 'severe' | 'unknown';
|
|
105
|
+
/** Filters for {@link ReclaimOldClient.getSessionLogs}. Every one is optional
|
|
106
|
+
* and forwarded verbatim; the session id itself is the route's path param, so
|
|
107
|
+
* `providerId`/`deviceId` narrow WITHIN one session rather than searching
|
|
108
|
+
* across sessions. */
|
|
109
|
+
export interface SessionLogsQuery {
|
|
110
|
+
deviceId?: string;
|
|
111
|
+
source?: string;
|
|
112
|
+
/** The client-side logger NAME (log-stream's `log_type` column, the SDK's
|
|
113
|
+
* `LogEntry.type`) — not the event type. */
|
|
114
|
+
logType?: string;
|
|
115
|
+
logLevel?: SessionLogLevel;
|
|
116
|
+
providerId?: string;
|
|
117
|
+
/** ISO-8601. Omitting BOTH bounds makes the log-stream service default to
|
|
118
|
+
* the last 3 days. */
|
|
119
|
+
startTime?: string;
|
|
120
|
+
endTime?: string;
|
|
121
|
+
/** Substring match against the log line (`LIKE %…%`). */
|
|
122
|
+
logLine?: string;
|
|
123
|
+
/** Exact `LogEventType` name, for example `REQUEST_MATCHED`. */
|
|
124
|
+
eventType?: string;
|
|
125
|
+
/** Capped at 1000 by the log-stream service. */
|
|
126
|
+
limit?: number;
|
|
127
|
+
offset?: number;
|
|
128
|
+
includeCount?: boolean;
|
|
129
|
+
}
|
|
130
|
+
/** One raw log line, in the log-stream service's snake_case ClickHouse shape,
|
|
131
|
+
* passed through unchanged by the devtools backend. `event_type` is the SDK's
|
|
132
|
+
* `LogEventType` milestone marker (`REQUEST_MATCHED`,
|
|
133
|
+
* `CLAIM_CREATION_STARTED`, `PROOF_GENERATED`, …); it is `''` on the majority
|
|
134
|
+
* of lines, which carry no event. */
|
|
135
|
+
export interface SessionLogRow {
|
|
136
|
+
timestamp: string;
|
|
137
|
+
request_id: string;
|
|
138
|
+
session_id: string;
|
|
139
|
+
device_id: string;
|
|
140
|
+
source: string;
|
|
141
|
+
app_id: string;
|
|
142
|
+
provider_id: string;
|
|
143
|
+
log_type: string;
|
|
144
|
+
log_level: string;
|
|
145
|
+
log_line: string;
|
|
146
|
+
event_type?: string;
|
|
147
|
+
metadata?: string;
|
|
148
|
+
}
|
|
149
|
+
/** `GET /api/logger/:sessionId`'s `data` envelope. The backend queries every
|
|
150
|
+
* region that holds the session, concatenates the pages, sorts them
|
|
151
|
+
* newest-first and re-slices to `limit`, so `data` is one page and
|
|
152
|
+
* `totalCount` is the match count across all of them. */
|
|
153
|
+
export interface SessionLogsPage {
|
|
154
|
+
data: SessionLogRow[];
|
|
155
|
+
count: number;
|
|
156
|
+
totalCount?: number;
|
|
157
|
+
/** Regions whose log-stream query failed — the page is INCOMPLETE when
|
|
158
|
+
* this is present. */
|
|
159
|
+
failedRegions?: string[];
|
|
160
|
+
}
|
|
97
161
|
/**
|
|
98
162
|
* Minimal REST client for the OLD devtools Express backend (base `/api`).
|
|
99
163
|
* Deliberately NOT a clone of `@reclaimprotocol/client`'s operationId
|
|
@@ -133,6 +197,25 @@ export declare class ReclaimOldClient {
|
|
|
133
197
|
* `#request`). Attaches the caller identity when one is set; queried
|
|
134
198
|
* anonymously otherwise. */
|
|
135
199
|
getSessionAnalyticsLogs(sessionId: string): Promise<unknown>;
|
|
200
|
+
/**
|
|
201
|
+
* Raw log LINES for a verification session — the SDK's own diagnostic
|
|
202
|
+
* logging, the thing the dashboard's session "logs" tab renders, and a
|
|
203
|
+
* strictly richer signal than `getSessionAnalyticsLogs`'s milestone events.
|
|
204
|
+
*
|
|
205
|
+
* This is the devtools backend (`GET /api/logger/:sessionId`), NOT the
|
|
206
|
+
* analytics-logs host — hence `#request`, and hence the same identity and
|
|
207
|
+
* ownership rules as every other authed call here: the session's app must
|
|
208
|
+
* be one you created (admins excepted), or the route 404s.
|
|
209
|
+
*
|
|
210
|
+
* The backend fans the query out to the per-region log-stream services,
|
|
211
|
+
* which read the ClickHouse `logs` table. Two defaults applied down there
|
|
212
|
+
* bite hard if you don't override them:
|
|
213
|
+
* - no `startTime`/`endTime` → only the last 3 days are searched;
|
|
214
|
+
* - no `logLevel` → INFO and above only, so every FINE/CONFIG line is
|
|
215
|
+
* invisible.
|
|
216
|
+
* Entries are dropped entirely after 30 days (table TTL).
|
|
217
|
+
*/
|
|
218
|
+
getSessionLogs(sessionId: string, query?: SessionLogsQuery): Promise<SessionLogsPage>;
|
|
136
219
|
/** Mint a brand-new, unassociated (sandboxed) app — no auth required. Link
|
|
137
220
|
* it to an account afterwards with `getLinkNonce` + `linkAppToAccount`, or
|
|
138
221
|
* use it as-is within the sandbox session limit. */
|
package/lib/old/client.js
CHANGED
|
@@ -89,6 +89,52 @@ export class ReclaimOldClient {
|
|
|
89
89
|
}
|
|
90
90
|
return json;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Raw log LINES for a verification session — the SDK's own diagnostic
|
|
94
|
+
* logging, the thing the dashboard's session "logs" tab renders, and a
|
|
95
|
+
* strictly richer signal than `getSessionAnalyticsLogs`'s milestone events.
|
|
96
|
+
*
|
|
97
|
+
* This is the devtools backend (`GET /api/logger/:sessionId`), NOT the
|
|
98
|
+
* analytics-logs host — hence `#request`, and hence the same identity and
|
|
99
|
+
* ownership rules as every other authed call here: the session's app must
|
|
100
|
+
* be one you created (admins excepted), or the route 404s.
|
|
101
|
+
*
|
|
102
|
+
* The backend fans the query out to the per-region log-stream services,
|
|
103
|
+
* which read the ClickHouse `logs` table. Two defaults applied down there
|
|
104
|
+
* bite hard if you don't override them:
|
|
105
|
+
* - no `startTime`/`endTime` → only the last 3 days are searched;
|
|
106
|
+
* - no `logLevel` → INFO and above only, so every FINE/CONFIG line is
|
|
107
|
+
* invisible.
|
|
108
|
+
* Entries are dropped entirely after 30 days (table TTL).
|
|
109
|
+
*/
|
|
110
|
+
async getSessionLogs(sessionId, query = {}) {
|
|
111
|
+
const res = await __classPrivateFieldGet(this, _ReclaimOldClient_instances, "m", _ReclaimOldClient_request).call(this, 'GET', `/logger/${encodeURIComponent(sessionId)}`, {
|
|
112
|
+
auth: true,
|
|
113
|
+
query: {
|
|
114
|
+
deviceId: query.deviceId,
|
|
115
|
+
source: query.source,
|
|
116
|
+
logType: query.logType,
|
|
117
|
+
logLevel: query.logLevel,
|
|
118
|
+
providerId: query.providerId,
|
|
119
|
+
startTime: query.startTime,
|
|
120
|
+
endTime: query.endTime,
|
|
121
|
+
logLine: query.logLine,
|
|
122
|
+
eventType: query.eventType,
|
|
123
|
+
limit: query.limit,
|
|
124
|
+
offset: query.offset,
|
|
125
|
+
includeCount: query.includeCount === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: String(query.includeCount),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const page = res.data;
|
|
131
|
+
return {
|
|
132
|
+
data: page?.data ?? [],
|
|
133
|
+
count: page?.count ?? page?.data?.length ?? 0,
|
|
134
|
+
totalCount: page?.totalCount,
|
|
135
|
+
failedRegions: page?.failedRegions,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
92
138
|
/** Mint a brand-new, unassociated (sandboxed) app — no auth required. Link
|
|
93
139
|
* it to an account afterwards with `getLinkNonce` + `linkAppToAccount`, or
|
|
94
140
|
* use it as-is within the sandbox session limit. */
|
package/lib/old/tools/build.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getMeProvidersTool } from "./me-providers.js";
|
|
|
8
8
|
import { createVersionFromCaptureTool } from "./publish.js";
|
|
9
9
|
import { scaffoldDemoServerTool } from "./scaffold-demo-server.js";
|
|
10
10
|
import { sessionAnalyticsLogsTool } from "./session-analytics-logs.js";
|
|
11
|
+
import { sessionLogsTool } from "./session-logs.js";
|
|
11
12
|
import { updateProviderMetadataTool } from "./update-provider-metadata.js";
|
|
12
13
|
import { verifySdkSetupTool } from "./verify-sdk-setup.js";
|
|
13
14
|
/**
|
|
@@ -32,6 +33,7 @@ export function buildOldBackendTools(client, sdkClient, attachRef) {
|
|
|
32
33
|
getProviderInfoTool(sdkClient),
|
|
33
34
|
updateProviderMetadataTool(client),
|
|
34
35
|
sessionAnalyticsLogsTool(client),
|
|
36
|
+
sessionLogsTool(client),
|
|
35
37
|
issueAppCredentialsTool(client),
|
|
36
38
|
linkAppTool(client),
|
|
37
39
|
checkAppStatusTool(client),
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { defineTool } from "../../mcp/server.js";
|
|
4
|
+
import { findProjectRoot } from "../../paths.js";
|
|
5
|
+
/** The log-stream service caps a page at 1000 entries however large a
|
|
6
|
+
* `limit` asks for, so paging is the only way past it. */
|
|
7
|
+
const MAX_PAGE = 1000;
|
|
8
|
+
/** Inline default. Small enough to stay readable in a tool result; a bigger
|
|
9
|
+
* pull belongs in `saveTo`. */
|
|
10
|
+
const DEFAULT_LIMIT = 200;
|
|
11
|
+
/** Ceiling on a `saveTo` dump, so a chatty session can't page forever. */
|
|
12
|
+
const DEFAULT_MAX_ENTRIES = 20000;
|
|
13
|
+
export function sessionLogsTool(client) {
|
|
14
|
+
return defineTool({
|
|
15
|
+
name: 'session_logs',
|
|
16
|
+
description: 'Fetch the raw log entries the in-app SDK emitted during a '
|
|
17
|
+
+ 'verification session, each with its event type (for example '
|
|
18
|
+
+ 'REQUEST_MATCHED, CLAIM_CREATION_STARTED, or PROOF_GENERATED), '
|
|
19
|
+
+ 'log level, and logger name. This is the data behind the '
|
|
20
|
+
+ 'dashboard session-logs tab, and it is far more detailed than '
|
|
21
|
+
+ 'session_analytics_logs, which returns only the coarse milestone '
|
|
22
|
+
+ 'events. Use it to diagnose a session that stalled, matched '
|
|
23
|
+
+ 'nothing, or produced no claim. Narrow the result with eventType, '
|
|
24
|
+
+ 'logLevel, or contains; page through it with limit and offset; or '
|
|
25
|
+
+ 'set saveTo to write every matching entry to an NDJSON file and '
|
|
26
|
+
+ 'read that file with your own tools. Old-devtools mode only. You '
|
|
27
|
+
+ 'must own the app the session belongs to, so call '
|
|
28
|
+
+ 'reclaim_authenticate first.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
sessionId: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Id of the verification session to read log entries for.',
|
|
35
|
+
},
|
|
36
|
+
eventType: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Keep only entries that carry this exact event type, for '
|
|
39
|
+
+ 'example REQUEST_MATCHED or PROOF_GENERATION_FAILED_'
|
|
40
|
+
+ 'EXCEPTION. Most entries carry no event type at all, so '
|
|
41
|
+
+ 'prefer eventsOnly when you want the milestone timeline '
|
|
42
|
+
+ 'rather than guessing a name.',
|
|
43
|
+
},
|
|
44
|
+
eventsOnly: {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
description: 'Keep only entries that carry an event type, which gives you '
|
|
47
|
+
+ 'a compact milestone timeline for the session. This tool '
|
|
48
|
+
+ 'applies the filter after fetching, so it narrows what you '
|
|
49
|
+
+ 'read rather than what it fetches.',
|
|
50
|
+
},
|
|
51
|
+
logLevel: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
enum: ['fine', 'config', 'info', 'warning', 'severe', 'unknown'],
|
|
54
|
+
description: 'Lowest severity to include. Each value is hierarchical: '
|
|
55
|
+
+ '`fine` includes FINE, CONFIG, INFO, WARNING, and SEVERE; '
|
|
56
|
+
+ '`info` drops FINE and CONFIG; `severe` keeps only '
|
|
57
|
+
+ 'failures. Defaults to `fine`, which is wider than the '
|
|
58
|
+
+ 'backend default of `info`. The FINER and FINEST tiers '
|
|
59
|
+
+ 'contain PII, and this route rejects them.',
|
|
60
|
+
},
|
|
61
|
+
logType: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Exact client-side logger name, which the SDK calls `type` — '
|
|
64
|
+
+ 'for example reclaim_flutter_sdk.ClaimCreation. This is not '
|
|
65
|
+
+ 'the event type.',
|
|
66
|
+
},
|
|
67
|
+
contains: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Substring that an entry\'s log line must contain.',
|
|
70
|
+
},
|
|
71
|
+
providerId: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Keep only entries for this provider. A session can touch '
|
|
74
|
+
+ 'more than one provider, so this narrows within the '
|
|
75
|
+
+ 'session; it cannot search across sessions.',
|
|
76
|
+
},
|
|
77
|
+
deviceId: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'Keep only entries for this device id, which is the claimant '
|
|
80
|
+
+ 'id.',
|
|
81
|
+
},
|
|
82
|
+
source: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Keep only entries from this client build. The source string '
|
|
85
|
+
+ 'carries the SDK, platform, and version, so it is the field '
|
|
86
|
+
+ 'to compare a working run against a broken one.',
|
|
87
|
+
},
|
|
88
|
+
startTime: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Lower bound, as an ISO-8601 timestamp. If you set neither '
|
|
91
|
+
+ 'bound, the backend searches only the last 3 days.',
|
|
92
|
+
},
|
|
93
|
+
endTime: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Upper bound, as an ISO-8601 timestamp.',
|
|
96
|
+
},
|
|
97
|
+
limit: {
|
|
98
|
+
type: 'number',
|
|
99
|
+
description: `Log entries per page. Defaults to ${DEFAULT_LIMIT} and is `
|
|
100
|
+
+ `capped at ${MAX_PAGE}. Ignored when you set saveTo.`,
|
|
101
|
+
},
|
|
102
|
+
offset: {
|
|
103
|
+
type: 'number',
|
|
104
|
+
description: 'Entries to skip, counting back from the newest match. '
|
|
105
|
+
+ 'Ignored when you set saveTo.',
|
|
106
|
+
},
|
|
107
|
+
saveTo: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Path to write every matching entry to, as NDJSON: one raw '
|
|
110
|
+
+ 'entry per line, oldest first. When you set this, the tool '
|
|
111
|
+
+ 'returns a summary instead of the entries. Relative paths '
|
|
112
|
+
+ 'resolve against the project root. Use it when a session '
|
|
113
|
+
+ 'has more entries than you want to read inline.',
|
|
114
|
+
},
|
|
115
|
+
maxEntries: {
|
|
116
|
+
type: 'number',
|
|
117
|
+
description: 'Maximum entries a saveTo dump collects. Defaults to '
|
|
118
|
+
+ `${DEFAULT_MAX_ENTRIES}.`,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ['sessionId'],
|
|
122
|
+
},
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
// The backend's own default is `info`, which silently hides every
|
|
125
|
+
// FINE and CONFIG entry — the ones that say what the interceptor
|
|
126
|
+
// actually saw. Debugging is the whole point of this tool, so widen
|
|
127
|
+
// the default here.
|
|
128
|
+
const query = {
|
|
129
|
+
logLevel: (args.logLevel ?? 'fine'),
|
|
130
|
+
logType: args.logType,
|
|
131
|
+
eventType: args.eventType,
|
|
132
|
+
providerId: args.providerId,
|
|
133
|
+
deviceId: args.deviceId,
|
|
134
|
+
source: args.source,
|
|
135
|
+
startTime: args.startTime,
|
|
136
|
+
endTime: args.endTime,
|
|
137
|
+
logLine: args.contains,
|
|
138
|
+
};
|
|
139
|
+
return args.saveTo
|
|
140
|
+
? await dump(client, args, query, args.saveTo)
|
|
141
|
+
: await page(client, args, query);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** One page, returned inline. */
|
|
145
|
+
async function page(client, args, query) {
|
|
146
|
+
const limit = clampLimit(args.limit ?? DEFAULT_LIMIT);
|
|
147
|
+
const offset = Math.max(args.offset ?? 0, 0);
|
|
148
|
+
const res = await client.getSessionLogs(args.sessionId, {
|
|
149
|
+
...query,
|
|
150
|
+
limit,
|
|
151
|
+
offset,
|
|
152
|
+
includeCount: true,
|
|
153
|
+
});
|
|
154
|
+
// Newest-first off the wire; a debugger reads a session forwards.
|
|
155
|
+
const chronological = [...res.data].reverse();
|
|
156
|
+
const entries = args.eventsOnly
|
|
157
|
+
? chronological.filter(hasEvent)
|
|
158
|
+
: chronological;
|
|
159
|
+
const fetched = res.data.length;
|
|
160
|
+
const total = res.totalCount;
|
|
161
|
+
return {
|
|
162
|
+
...summarize(args, res.failedRegions, entries, total),
|
|
163
|
+
page: {
|
|
164
|
+
limit,
|
|
165
|
+
offset,
|
|
166
|
+
fetched,
|
|
167
|
+
...(args.eventsOnly ? { keptWithEvent: entries.length } : {}),
|
|
168
|
+
hasMore: total === undefined
|
|
169
|
+
? fetched === limit
|
|
170
|
+
: offset + fetched < total,
|
|
171
|
+
nextOffset: offset + fetched,
|
|
172
|
+
},
|
|
173
|
+
entries: compact(entries),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Every match, paged out to an NDJSON file. */
|
|
177
|
+
async function dump(client, args, query, saveTo) {
|
|
178
|
+
const maxEntries = Math.max(args.maxEntries ?? DEFAULT_MAX_ENTRIES, 1);
|
|
179
|
+
const all = [];
|
|
180
|
+
const failedRegions = new Set();
|
|
181
|
+
let total;
|
|
182
|
+
let offset = 0;
|
|
183
|
+
while (all.length < maxEntries) {
|
|
184
|
+
const res = await client.getSessionLogs(args.sessionId, {
|
|
185
|
+
...query,
|
|
186
|
+
limit: Math.min(MAX_PAGE, maxEntries - all.length),
|
|
187
|
+
offset,
|
|
188
|
+
// Only the first page pays for the count; it does not move.
|
|
189
|
+
includeCount: offset === 0,
|
|
190
|
+
});
|
|
191
|
+
for (const region of res.failedRegions ?? []) {
|
|
192
|
+
failedRegions.add(region);
|
|
193
|
+
}
|
|
194
|
+
if (offset === 0) {
|
|
195
|
+
total = res.totalCount;
|
|
196
|
+
}
|
|
197
|
+
all.push(...res.data);
|
|
198
|
+
offset += res.data.length;
|
|
199
|
+
// A short page means the match set is exhausted.
|
|
200
|
+
if (!res.data.length || res.data.length < MAX_PAGE) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
if (total !== undefined && offset >= total) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
all.reverse();
|
|
208
|
+
const entries = args.eventsOnly ? all.filter(hasEvent) : all;
|
|
209
|
+
const path = isAbsolute(saveTo)
|
|
210
|
+
? saveTo
|
|
211
|
+
: join(findProjectRoot(process.cwd()), saveTo);
|
|
212
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
213
|
+
writeFileSync(path, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
214
|
+
return {
|
|
215
|
+
...summarize(args, [...failedRegions], entries, total),
|
|
216
|
+
file: {
|
|
217
|
+
path,
|
|
218
|
+
format: 'ndjson',
|
|
219
|
+
entries: entries.length,
|
|
220
|
+
order: 'oldest first',
|
|
221
|
+
note: 'Each line holds one raw log entry with all of its columns: '
|
|
222
|
+
+ 'timestamp, log_level, event_type, log_type, log_line, '
|
|
223
|
+
+ 'session_id, app_id, provider_id, device_id, source, and '
|
|
224
|
+
+ 'metadata.',
|
|
225
|
+
truncated: entries.length >= maxEntries
|
|
226
|
+
? `Stopped at maxEntries=${maxEntries}. Narrow the filters, or `
|
|
227
|
+
+ 'raise the limit.'
|
|
228
|
+
: undefined,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** The header both modes share: what was asked for, what came back, and the
|
|
233
|
+
* event-type histogram — usually enough on its own to see where a session
|
|
234
|
+
* died. */
|
|
235
|
+
function summarize(args, failedRegions, entries, total) {
|
|
236
|
+
const events = {};
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
if (entry.event_type) {
|
|
239
|
+
events[entry.event_type] = (events[entry.event_type] ?? 0) + 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
sessionId: args.sessionId,
|
|
244
|
+
context: contextOf(entries),
|
|
245
|
+
totalMatching: total,
|
|
246
|
+
span: entries.length
|
|
247
|
+
? {
|
|
248
|
+
from: entries[0].timestamp,
|
|
249
|
+
to: entries[entries.length - 1].timestamp,
|
|
250
|
+
}
|
|
251
|
+
: undefined,
|
|
252
|
+
eventTypes: Object.keys(events).length ? events : undefined,
|
|
253
|
+
...(failedRegions?.length
|
|
254
|
+
? {
|
|
255
|
+
warning: 'The log-stream query failed in region '
|
|
256
|
+
+ `${failedRegions.join(', ')}, so this result is incomplete.`,
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
...(entries.length
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
hint: 'No entries matched. Check these causes in order: the '
|
|
263
|
+
+ 'search window defaults to the last 3 days, so pass startTime '
|
|
264
|
+
+ 'and endTime for an older session; the backend deletes entries '
|
|
265
|
+
+ 'after 30 days; the backend picks which regions to query from '
|
|
266
|
+
+ 'the analytics table, so a session that never recorded an '
|
|
267
|
+
+ 'analytics event reads as empty here even when entries exist; '
|
|
268
|
+
+ 'or the filters are too narrow. Cross-check with '
|
|
269
|
+
+ 'session_analytics_logs.',
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Fields that are identical on every entry, hoisted out so they are not
|
|
274
|
+
* repeated hundreds of times. */
|
|
275
|
+
function contextOf(entries) {
|
|
276
|
+
const uniq = (pick) => {
|
|
277
|
+
const set = new Set(entries.map(pick).filter(Boolean));
|
|
278
|
+
return [...set];
|
|
279
|
+
};
|
|
280
|
+
const one = (values) => (values.length === 1 ? values[0] : values);
|
|
281
|
+
return entries.length
|
|
282
|
+
? {
|
|
283
|
+
appId: one(uniq(e => e.app_id)),
|
|
284
|
+
providerId: one(uniq(e => e.provider_id)),
|
|
285
|
+
deviceId: one(uniq(e => e.device_id)),
|
|
286
|
+
source: one(uniq(e => e.source)),
|
|
287
|
+
}
|
|
288
|
+
: undefined;
|
|
289
|
+
}
|
|
290
|
+
function compact(entries) {
|
|
291
|
+
const devices = new Set(entries.map(e => e.device_id));
|
|
292
|
+
const sources = new Set(entries.map(e => e.source));
|
|
293
|
+
return entries.map(e => ({
|
|
294
|
+
ts: e.timestamp,
|
|
295
|
+
level: e.log_level,
|
|
296
|
+
...(e.event_type ? { event: e.event_type } : {}),
|
|
297
|
+
logger: e.log_type,
|
|
298
|
+
line: e.log_line,
|
|
299
|
+
// Only worth repeating per entry once the page actually mixes them.
|
|
300
|
+
...(devices.size > 1 ? { device: e.device_id } : {}),
|
|
301
|
+
...(sources.size > 1 ? { source: e.source } : {}),
|
|
302
|
+
...(e.metadata && e.metadata !== '{}' ? { metadata: e.metadata } : {}),
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
function hasEvent(entry) {
|
|
306
|
+
return !!entry.event_type;
|
|
307
|
+
}
|
|
308
|
+
function clampLimit(limit) {
|
|
309
|
+
return Math.min(Math.max(Math.trunc(limit), 1), MAX_PAGE);
|
|
310
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/agent",
|
|
3
3
|
"title": "Reclaim Agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.26",
|
|
5
5
|
"description": "Reclaim Protocol tools for AI coding agents. Reclaim lets your app verify data points about its users, such as a follower count, an account balance, or an order history. This package is the Reclaim MCP server: it gives your agent the tools to find, build, test, and fix data verification providers by capturing and replaying browser requests, drafting provider recipes, running zkTLS proofs, and publishing provider versions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
84
84
|
"@noble/hashes": "^2.2.0",
|
|
85
85
|
"@reclaimprotocol/attestor-core": "^5.0.8",
|
|
86
|
-
"@reclaimprotocol/client": "0.1.
|
|
86
|
+
"@reclaimprotocol/client": "0.1.26",
|
|
87
87
|
"@reclaimprotocol/tls": "^0.1.3",
|
|
88
88
|
"chrome-remote-interface": "^0.33.0",
|
|
89
89
|
"domhandler": "^6.0.0",
|
package/skill/SKILL.old.md
CHANGED
|
@@ -37,8 +37,9 @@ workflow — the narrow `authenticate_builder` / `list_builder_organizations`
|
|
|
37
37
|
exception exists only to allocate Builder-hosted authoring browsers.
|
|
38
38
|
`get_me_providers` is the only provider-listing tool here.
|
|
39
39
|
|
|
40
|
-
**Diagnosing a session:** `session_analytics_logs`
|
|
41
|
-
|
|
40
|
+
**Diagnosing a session:** `session_analytics_logs` returns a session's
|
|
41
|
+
milestone events; `session_logs` returns the log entries the SDK itself
|
|
42
|
+
emitted. Read both from topic `troubleshooting`.
|
|
42
43
|
|
|
43
44
|
<!-- topic: auth -->
|
|
44
45
|
## Getting authenticated
|
|
@@ -357,6 +358,31 @@ breaks the page, and it's the smallest change. Only if that doesn't help, turn
|
|
|
357
358
|
interception off entirely (`injectionType: NONE`, or omit
|
|
358
359
|
`interceptorOptions`). Full checklist and remedies: topic `interception`.
|
|
359
360
|
|
|
361
|
+
**A session stalled, matched nothing, or produced no claim.** Read it with
|
|
362
|
+
two tools, coarse to fine. `session_analytics_logs` returns the milestone
|
|
363
|
+
events for a session id — SESSION_STARTED, PROOF_GENERATION_FAILED, and so on
|
|
364
|
+
— which tells you how far the session got. `session_logs` returns the log
|
|
365
|
+
entries the SDK emitted, each with its event type, log level, and logger name,
|
|
366
|
+
which tells you why it stopped: the request it intercepted, the match or
|
|
367
|
+
redaction that failed, and what the attestor answered. Work through it in this
|
|
368
|
+
order:
|
|
369
|
+
|
|
370
|
+
1. Call `session_analytics_logs({ sessionId })` to place the failure on the
|
|
371
|
+
timeline.
|
|
372
|
+
2. Call `session_logs({ sessionId, eventsOnly: true })` for the same timeline
|
|
373
|
+
at entry level, then drop `eventsOnly` and add `contains` or `eventType` to
|
|
374
|
+
read around the entry that failed.
|
|
375
|
+
3. To compare a working run against a broken one, filter by `source` — it
|
|
376
|
+
carries the SDK, platform, and version, which is usually the variable that
|
|
377
|
+
differs.
|
|
378
|
+
|
|
379
|
+
Two defaults decide whether you see anything: the backend searches only the
|
|
380
|
+
last 3 days unless you pass `startTime` and `endTime`, and it deletes entries
|
|
381
|
+
after 30 days. An empty result is more often one of those than a genuinely
|
|
382
|
+
silent session, so read the `hint` the tool returns before concluding
|
|
383
|
+
anything. When a session is too large to read inline, pass `saveTo` to write
|
|
384
|
+
every entry to an NDJSON file and grep that file instead.
|
|
385
|
+
|
|
360
386
|
**Proof failures / attestor errors** → topic `prove`.
|
|
361
387
|
**Browser won't launch or attach** → topic `browser`.
|
|
362
388
|
**Missing signing key** → topic `credentials`.
|
package/skill/old-mode.md
CHANGED
|
@@ -52,8 +52,10 @@ and the proof binds to nobody — find an auth-bound request instead.
|
|
|
52
52
|
Values the CONSUMER supplies at verification time use `{{context_<name>}}` —
|
|
53
53
|
this backend does NOT support the builder's dotted `{{context.<name>}}` form.
|
|
54
54
|
|
|
55
|
-
**Diagnosing a session:** `session_analytics_logs`
|
|
56
|
-
|
|
55
|
+
**Diagnosing a session:** `session_analytics_logs` returns a session's
|
|
56
|
+
milestone events; `session_logs` returns the log entries the SDK emitted, each
|
|
57
|
+
with its event type — that is where a failed match, redaction, or claim shows
|
|
58
|
+
up. Both take a `sessionId`; see topic `troubleshooting` for how to read them.
|
|
57
59
|
|
|
58
60
|
**Backend mode:** the BUILDER backend (generated API tools, orgs, verification
|
|
59
61
|
sessions, public review) is one switch away. `get_devtools_mode` reports the
|
package/src/old/client.ts
CHANGED
|
@@ -99,6 +99,80 @@ export interface LinkNonce {
|
|
|
99
99
|
expiresInSeconds: number
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** Log-level filter accepted by the devtools backend's session-logs route.
|
|
103
|
+
* It validates against this exact list and rejects anything else with a 400,
|
|
104
|
+
* including the comma-separated lists the log-stream service itself would
|
|
105
|
+
* accept. Each value is hierarchical at the log-stream end: `fine` means
|
|
106
|
+
* FINE+CONFIG+INFO+WARNING+SEVERE, `info` means INFO+WARNING+SEVERE, and so
|
|
107
|
+
* on. `finer`/`finest` (the PII tiers) are not reachable through this route
|
|
108
|
+
* at all. */
|
|
109
|
+
export type SessionLogLevel =
|
|
110
|
+
| 'fine'
|
|
111
|
+
| 'config'
|
|
112
|
+
| 'info'
|
|
113
|
+
| 'warning'
|
|
114
|
+
| 'severe'
|
|
115
|
+
| 'unknown'
|
|
116
|
+
|
|
117
|
+
/** Filters for {@link ReclaimOldClient.getSessionLogs}. Every one is optional
|
|
118
|
+
* and forwarded verbatim; the session id itself is the route's path param, so
|
|
119
|
+
* `providerId`/`deviceId` narrow WITHIN one session rather than searching
|
|
120
|
+
* across sessions. */
|
|
121
|
+
export interface SessionLogsQuery {
|
|
122
|
+
deviceId?: string
|
|
123
|
+
source?: string
|
|
124
|
+
/** The client-side logger NAME (log-stream's `log_type` column, the SDK's
|
|
125
|
+
* `LogEntry.type`) — not the event type. */
|
|
126
|
+
logType?: string
|
|
127
|
+
logLevel?: SessionLogLevel
|
|
128
|
+
providerId?: string
|
|
129
|
+
/** ISO-8601. Omitting BOTH bounds makes the log-stream service default to
|
|
130
|
+
* the last 3 days. */
|
|
131
|
+
startTime?: string
|
|
132
|
+
endTime?: string
|
|
133
|
+
/** Substring match against the log line (`LIKE %…%`). */
|
|
134
|
+
logLine?: string
|
|
135
|
+
/** Exact `LogEventType` name, for example `REQUEST_MATCHED`. */
|
|
136
|
+
eventType?: string
|
|
137
|
+
/** Capped at 1000 by the log-stream service. */
|
|
138
|
+
limit?: number
|
|
139
|
+
offset?: number
|
|
140
|
+
includeCount?: boolean
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One raw log line, in the log-stream service's snake_case ClickHouse shape,
|
|
144
|
+
* passed through unchanged by the devtools backend. `event_type` is the SDK's
|
|
145
|
+
* `LogEventType` milestone marker (`REQUEST_MATCHED`,
|
|
146
|
+
* `CLAIM_CREATION_STARTED`, `PROOF_GENERATED`, …); it is `''` on the majority
|
|
147
|
+
* of lines, which carry no event. */
|
|
148
|
+
export interface SessionLogRow {
|
|
149
|
+
timestamp: string
|
|
150
|
+
request_id: string
|
|
151
|
+
session_id: string
|
|
152
|
+
device_id: string
|
|
153
|
+
source: string
|
|
154
|
+
app_id: string
|
|
155
|
+
provider_id: string
|
|
156
|
+
log_type: string
|
|
157
|
+
log_level: string
|
|
158
|
+
log_line: string
|
|
159
|
+
event_type?: string
|
|
160
|
+
metadata?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `GET /api/logger/:sessionId`'s `data` envelope. The backend queries every
|
|
164
|
+
* region that holds the session, concatenates the pages, sorts them
|
|
165
|
+
* newest-first and re-slices to `limit`, so `data` is one page and
|
|
166
|
+
* `totalCount` is the match count across all of them. */
|
|
167
|
+
export interface SessionLogsPage {
|
|
168
|
+
data: SessionLogRow[]
|
|
169
|
+
count: number
|
|
170
|
+
totalCount?: number
|
|
171
|
+
/** Regions whose log-stream query failed — the page is INCOMPLETE when
|
|
172
|
+
* this is present. */
|
|
173
|
+
failedRegions?: string[]
|
|
174
|
+
}
|
|
175
|
+
|
|
102
176
|
interface RequestOpts {
|
|
103
177
|
query?: Record<string, string | number | undefined>
|
|
104
178
|
body?: unknown
|
|
@@ -212,6 +286,60 @@ export class ReclaimOldClient {
|
|
|
212
286
|
return json
|
|
213
287
|
}
|
|
214
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Raw log LINES for a verification session — the SDK's own diagnostic
|
|
291
|
+
* logging, the thing the dashboard's session "logs" tab renders, and a
|
|
292
|
+
* strictly richer signal than `getSessionAnalyticsLogs`'s milestone events.
|
|
293
|
+
*
|
|
294
|
+
* This is the devtools backend (`GET /api/logger/:sessionId`), NOT the
|
|
295
|
+
* analytics-logs host — hence `#request`, and hence the same identity and
|
|
296
|
+
* ownership rules as every other authed call here: the session's app must
|
|
297
|
+
* be one you created (admins excepted), or the route 404s.
|
|
298
|
+
*
|
|
299
|
+
* The backend fans the query out to the per-region log-stream services,
|
|
300
|
+
* which read the ClickHouse `logs` table. Two defaults applied down there
|
|
301
|
+
* bite hard if you don't override them:
|
|
302
|
+
* - no `startTime`/`endTime` → only the last 3 days are searched;
|
|
303
|
+
* - no `logLevel` → INFO and above only, so every FINE/CONFIG line is
|
|
304
|
+
* invisible.
|
|
305
|
+
* Entries are dropped entirely after 30 days (table TTL).
|
|
306
|
+
*/
|
|
307
|
+
async getSessionLogs(
|
|
308
|
+
sessionId: string,
|
|
309
|
+
query: SessionLogsQuery = {},
|
|
310
|
+
): Promise<SessionLogsPage> {
|
|
311
|
+
const res = await this.#request(
|
|
312
|
+
'GET',
|
|
313
|
+
`/logger/${encodeURIComponent(sessionId)}`,
|
|
314
|
+
{
|
|
315
|
+
auth: true,
|
|
316
|
+
query: {
|
|
317
|
+
deviceId: query.deviceId,
|
|
318
|
+
source: query.source,
|
|
319
|
+
logType: query.logType,
|
|
320
|
+
logLevel: query.logLevel,
|
|
321
|
+
providerId: query.providerId,
|
|
322
|
+
startTime: query.startTime,
|
|
323
|
+
endTime: query.endTime,
|
|
324
|
+
logLine: query.logLine,
|
|
325
|
+
eventType: query.eventType,
|
|
326
|
+
limit: query.limit,
|
|
327
|
+
offset: query.offset,
|
|
328
|
+
includeCount: query.includeCount === undefined
|
|
329
|
+
? undefined
|
|
330
|
+
: String(query.includeCount),
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
const page = (res as { data?: Partial<SessionLogsPage> }).data
|
|
335
|
+
return {
|
|
336
|
+
data: page?.data ?? [],
|
|
337
|
+
count: page?.count ?? page?.data?.length ?? 0,
|
|
338
|
+
totalCount: page?.totalCount,
|
|
339
|
+
failedRegions: page?.failedRegions,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
215
343
|
/** Mint a brand-new, unassociated (sandboxed) app — no auth required. Link
|
|
216
344
|
* it to an account afterwards with `getLinkNonce` + `linkAppToAccount`, or
|
|
217
345
|
* use it as-is within the sandbox session limit. */
|
package/src/old/tools/build.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { getMeProvidersTool } from './me-providers.ts'
|
|
|
12
12
|
import { createVersionFromCaptureTool } from './publish.ts'
|
|
13
13
|
import { scaffoldDemoServerTool } from './scaffold-demo-server.ts'
|
|
14
14
|
import { sessionAnalyticsLogsTool } from './session-analytics-logs.ts'
|
|
15
|
+
import { sessionLogsTool } from './session-logs.ts'
|
|
15
16
|
import { updateProviderMetadataTool } from './update-provider-metadata.ts'
|
|
16
17
|
import { verifySdkSetupTool } from './verify-sdk-setup.ts'
|
|
17
18
|
|
|
@@ -41,6 +42,7 @@ export function buildOldBackendTools(
|
|
|
41
42
|
getProviderInfoTool(sdkClient),
|
|
42
43
|
updateProviderMetadataTool(client),
|
|
43
44
|
sessionAnalyticsLogsTool(client),
|
|
45
|
+
sessionLogsTool(client),
|
|
44
46
|
issueAppCredentialsTool(client),
|
|
45
47
|
linkAppTool(client),
|
|
46
48
|
checkAppStatusTool(client),
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
3
|
+
import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
|
|
4
|
+
import { findProjectRoot } from '../../paths.ts'
|
|
5
|
+
import type {
|
|
6
|
+
ReclaimOldClient,
|
|
7
|
+
SessionLogRow,
|
|
8
|
+
SessionLogsQuery,
|
|
9
|
+
} from '../client.ts'
|
|
10
|
+
|
|
11
|
+
/** The log-stream service caps a page at 1000 entries however large a
|
|
12
|
+
* `limit` asks for, so paging is the only way past it. */
|
|
13
|
+
const MAX_PAGE = 1000
|
|
14
|
+
|
|
15
|
+
/** Inline default. Small enough to stay readable in a tool result; a bigger
|
|
16
|
+
* pull belongs in `saveTo`. */
|
|
17
|
+
const DEFAULT_LIMIT = 200
|
|
18
|
+
|
|
19
|
+
/** Ceiling on a `saveTo` dump, so a chatty session can't page forever. */
|
|
20
|
+
const DEFAULT_MAX_ENTRIES = 20_000
|
|
21
|
+
|
|
22
|
+
interface Args extends SessionLogsQuery {
|
|
23
|
+
sessionId: string
|
|
24
|
+
/** Substring match on the log line — `logLine` under its dashboard name. */
|
|
25
|
+
contains?: string
|
|
26
|
+
/** Keep only entries that carry an `event_type`. Applied here rather than
|
|
27
|
+
* server-side: the devtools route has no `eventTypeOnly` passthrough. */
|
|
28
|
+
eventsOnly?: boolean
|
|
29
|
+
saveTo?: string
|
|
30
|
+
maxEntries?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An entry's fields minus the ones hoisted into `context`. `event` and
|
|
34
|
+
* `metadata` drop out when empty; `device` and `source` appear only on an
|
|
35
|
+
* entry that disagrees with a single-valued context. */
|
|
36
|
+
interface CompactEntry {
|
|
37
|
+
ts: string
|
|
38
|
+
level: string
|
|
39
|
+
event?: string
|
|
40
|
+
logger: string
|
|
41
|
+
line: string
|
|
42
|
+
device?: string
|
|
43
|
+
source?: string
|
|
44
|
+
metadata?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sessionLogsTool(client: ReclaimOldClient): RegisteredTool {
|
|
48
|
+
return defineTool<Args>(
|
|
49
|
+
{
|
|
50
|
+
name: 'session_logs',
|
|
51
|
+
description:
|
|
52
|
+
'Fetch the raw log entries the in-app SDK emitted during a '
|
|
53
|
+
+ 'verification session, each with its event type (for example '
|
|
54
|
+
+ 'REQUEST_MATCHED, CLAIM_CREATION_STARTED, or PROOF_GENERATED), '
|
|
55
|
+
+ 'log level, and logger name. This is the data behind the '
|
|
56
|
+
+ 'dashboard session-logs tab, and it is far more detailed than '
|
|
57
|
+
+ 'session_analytics_logs, which returns only the coarse milestone '
|
|
58
|
+
+ 'events. Use it to diagnose a session that stalled, matched '
|
|
59
|
+
+ 'nothing, or produced no claim. Narrow the result with eventType, '
|
|
60
|
+
+ 'logLevel, or contains; page through it with limit and offset; or '
|
|
61
|
+
+ 'set saveTo to write every matching entry to an NDJSON file and '
|
|
62
|
+
+ 'read that file with your own tools. Old-devtools mode only. You '
|
|
63
|
+
+ 'must own the app the session belongs to, so call '
|
|
64
|
+
+ 'reclaim_authenticate first.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
sessionId: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description:
|
|
71
|
+
'Id of the verification session to read log entries for.',
|
|
72
|
+
},
|
|
73
|
+
eventType: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description:
|
|
76
|
+
'Keep only entries that carry this exact event type, for '
|
|
77
|
+
+ 'example REQUEST_MATCHED or PROOF_GENERATION_FAILED_'
|
|
78
|
+
+ 'EXCEPTION. Most entries carry no event type at all, so '
|
|
79
|
+
+ 'prefer eventsOnly when you want the milestone timeline '
|
|
80
|
+
+ 'rather than guessing a name.',
|
|
81
|
+
},
|
|
82
|
+
eventsOnly: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description:
|
|
85
|
+
'Keep only entries that carry an event type, which gives you '
|
|
86
|
+
+ 'a compact milestone timeline for the session. This tool '
|
|
87
|
+
+ 'applies the filter after fetching, so it narrows what you '
|
|
88
|
+
+ 'read rather than what it fetches.',
|
|
89
|
+
},
|
|
90
|
+
logLevel: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
enum: ['fine', 'config', 'info', 'warning', 'severe', 'unknown'],
|
|
93
|
+
description:
|
|
94
|
+
'Lowest severity to include. Each value is hierarchical: '
|
|
95
|
+
+ '`fine` includes FINE, CONFIG, INFO, WARNING, and SEVERE; '
|
|
96
|
+
+ '`info` drops FINE and CONFIG; `severe` keeps only '
|
|
97
|
+
+ 'failures. Defaults to `fine`, which is wider than the '
|
|
98
|
+
+ 'backend default of `info`. The FINER and FINEST tiers '
|
|
99
|
+
+ 'contain PII, and this route rejects them.',
|
|
100
|
+
},
|
|
101
|
+
logType: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description:
|
|
104
|
+
'Exact client-side logger name, which the SDK calls `type` — '
|
|
105
|
+
+ 'for example reclaim_flutter_sdk.ClaimCreation. This is not '
|
|
106
|
+
+ 'the event type.',
|
|
107
|
+
},
|
|
108
|
+
contains: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description:
|
|
111
|
+
'Substring that an entry\'s log line must contain.',
|
|
112
|
+
},
|
|
113
|
+
providerId: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description:
|
|
116
|
+
'Keep only entries for this provider. A session can touch '
|
|
117
|
+
+ 'more than one provider, so this narrows within the '
|
|
118
|
+
+ 'session; it cannot search across sessions.',
|
|
119
|
+
},
|
|
120
|
+
deviceId: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description:
|
|
123
|
+
'Keep only entries for this device id, which is the claimant '
|
|
124
|
+
+ 'id.',
|
|
125
|
+
},
|
|
126
|
+
source: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description:
|
|
129
|
+
'Keep only entries from this client build. The source string '
|
|
130
|
+
+ 'carries the SDK, platform, and version, so it is the field '
|
|
131
|
+
+ 'to compare a working run against a broken one.',
|
|
132
|
+
},
|
|
133
|
+
startTime: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description:
|
|
136
|
+
'Lower bound, as an ISO-8601 timestamp. If you set neither '
|
|
137
|
+
+ 'bound, the backend searches only the last 3 days.',
|
|
138
|
+
},
|
|
139
|
+
endTime: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'Upper bound, as an ISO-8601 timestamp.',
|
|
142
|
+
},
|
|
143
|
+
limit: {
|
|
144
|
+
type: 'number',
|
|
145
|
+
description:
|
|
146
|
+
`Log entries per page. Defaults to ${DEFAULT_LIMIT} and is `
|
|
147
|
+
+ `capped at ${MAX_PAGE}. Ignored when you set saveTo.`,
|
|
148
|
+
},
|
|
149
|
+
offset: {
|
|
150
|
+
type: 'number',
|
|
151
|
+
description:
|
|
152
|
+
'Entries to skip, counting back from the newest match. '
|
|
153
|
+
+ 'Ignored when you set saveTo.',
|
|
154
|
+
},
|
|
155
|
+
saveTo: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description:
|
|
158
|
+
'Path to write every matching entry to, as NDJSON: one raw '
|
|
159
|
+
+ 'entry per line, oldest first. When you set this, the tool '
|
|
160
|
+
+ 'returns a summary instead of the entries. Relative paths '
|
|
161
|
+
+ 'resolve against the project root. Use it when a session '
|
|
162
|
+
+ 'has more entries than you want to read inline.',
|
|
163
|
+
},
|
|
164
|
+
maxEntries: {
|
|
165
|
+
type: 'number',
|
|
166
|
+
description:
|
|
167
|
+
'Maximum entries a saveTo dump collects. Defaults to '
|
|
168
|
+
+ `${DEFAULT_MAX_ENTRIES}.`,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
required: ['sessionId'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
async(args) => {
|
|
175
|
+
// The backend's own default is `info`, which silently hides every
|
|
176
|
+
// FINE and CONFIG entry — the ones that say what the interceptor
|
|
177
|
+
// actually saw. Debugging is the whole point of this tool, so widen
|
|
178
|
+
// the default here.
|
|
179
|
+
const query: SessionLogsQuery = {
|
|
180
|
+
logLevel: (args.logLevel ?? 'fine'),
|
|
181
|
+
logType: args.logType,
|
|
182
|
+
eventType: args.eventType,
|
|
183
|
+
providerId: args.providerId,
|
|
184
|
+
deviceId: args.deviceId,
|
|
185
|
+
source: args.source,
|
|
186
|
+
startTime: args.startTime,
|
|
187
|
+
endTime: args.endTime,
|
|
188
|
+
logLine: args.contains,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return args.saveTo
|
|
192
|
+
? await dump(client, args, query, args.saveTo)
|
|
193
|
+
: await page(client, args, query)
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One page, returned inline. */
|
|
199
|
+
async function page(
|
|
200
|
+
client: ReclaimOldClient,
|
|
201
|
+
args: Args,
|
|
202
|
+
query: SessionLogsQuery,
|
|
203
|
+
) {
|
|
204
|
+
const limit = clampLimit(args.limit ?? DEFAULT_LIMIT)
|
|
205
|
+
const offset = Math.max(args.offset ?? 0, 0)
|
|
206
|
+
const res = await client.getSessionLogs(args.sessionId, {
|
|
207
|
+
...query,
|
|
208
|
+
limit,
|
|
209
|
+
offset,
|
|
210
|
+
includeCount: true,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Newest-first off the wire; a debugger reads a session forwards.
|
|
214
|
+
const chronological = [...res.data].reverse()
|
|
215
|
+
const entries = args.eventsOnly
|
|
216
|
+
? chronological.filter(hasEvent)
|
|
217
|
+
: chronological
|
|
218
|
+
const fetched = res.data.length
|
|
219
|
+
const total = res.totalCount
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
...summarize(args, res.failedRegions, entries, total),
|
|
223
|
+
page: {
|
|
224
|
+
limit,
|
|
225
|
+
offset,
|
|
226
|
+
fetched,
|
|
227
|
+
...(args.eventsOnly ? { keptWithEvent: entries.length } : {}),
|
|
228
|
+
hasMore: total === undefined
|
|
229
|
+
? fetched === limit
|
|
230
|
+
: offset + fetched < total,
|
|
231
|
+
nextOffset: offset + fetched,
|
|
232
|
+
},
|
|
233
|
+
entries: compact(entries),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Every match, paged out to an NDJSON file. */
|
|
238
|
+
async function dump(
|
|
239
|
+
client: ReclaimOldClient,
|
|
240
|
+
args: Args,
|
|
241
|
+
query: SessionLogsQuery,
|
|
242
|
+
saveTo: string,
|
|
243
|
+
) {
|
|
244
|
+
const maxEntries = Math.max(args.maxEntries ?? DEFAULT_MAX_ENTRIES, 1)
|
|
245
|
+
const all: SessionLogRow[] = []
|
|
246
|
+
const failedRegions = new Set<string>()
|
|
247
|
+
let total: number | undefined
|
|
248
|
+
let offset = 0
|
|
249
|
+
|
|
250
|
+
while(all.length < maxEntries) {
|
|
251
|
+
const res = await client.getSessionLogs(args.sessionId, {
|
|
252
|
+
...query,
|
|
253
|
+
limit: Math.min(MAX_PAGE, maxEntries - all.length),
|
|
254
|
+
offset,
|
|
255
|
+
// Only the first page pays for the count; it does not move.
|
|
256
|
+
includeCount: offset === 0,
|
|
257
|
+
})
|
|
258
|
+
for(const region of res.failedRegions ?? []) {
|
|
259
|
+
failedRegions.add(region)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if(offset === 0) {
|
|
263
|
+
total = res.totalCount
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
all.push(...res.data)
|
|
267
|
+
offset += res.data.length
|
|
268
|
+
// A short page means the match set is exhausted.
|
|
269
|
+
if(!res.data.length || res.data.length < MAX_PAGE) {
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if(total !== undefined && offset >= total) {
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
all.reverse()
|
|
279
|
+
const entries = args.eventsOnly ? all.filter(hasEvent) : all
|
|
280
|
+
|
|
281
|
+
const path = isAbsolute(saveTo)
|
|
282
|
+
? saveTo
|
|
283
|
+
: join(findProjectRoot(process.cwd()), saveTo)
|
|
284
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
285
|
+
writeFileSync(path, entries.map(e => JSON.stringify(e)).join('\n') + '\n')
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
...summarize(args, [...failedRegions], entries, total),
|
|
289
|
+
file: {
|
|
290
|
+
path,
|
|
291
|
+
format: 'ndjson',
|
|
292
|
+
entries: entries.length,
|
|
293
|
+
order: 'oldest first',
|
|
294
|
+
note: 'Each line holds one raw log entry with all of its columns: '
|
|
295
|
+
+ 'timestamp, log_level, event_type, log_type, log_line, '
|
|
296
|
+
+ 'session_id, app_id, provider_id, device_id, source, and '
|
|
297
|
+
+ 'metadata.',
|
|
298
|
+
truncated: entries.length >= maxEntries
|
|
299
|
+
? `Stopped at maxEntries=${maxEntries}. Narrow the filters, or `
|
|
300
|
+
+ 'raise the limit.'
|
|
301
|
+
: undefined,
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The header both modes share: what was asked for, what came back, and the
|
|
307
|
+
* event-type histogram — usually enough on its own to see where a session
|
|
308
|
+
* died. */
|
|
309
|
+
function summarize(
|
|
310
|
+
args: Args,
|
|
311
|
+
failedRegions: string[] | undefined,
|
|
312
|
+
entries: SessionLogRow[],
|
|
313
|
+
total: number | undefined,
|
|
314
|
+
) {
|
|
315
|
+
const events: Record<string, number> = {}
|
|
316
|
+
for(const entry of entries) {
|
|
317
|
+
if(entry.event_type) {
|
|
318
|
+
events[entry.event_type] = (events[entry.event_type] ?? 0) + 1
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
sessionId: args.sessionId,
|
|
324
|
+
context: contextOf(entries),
|
|
325
|
+
totalMatching: total,
|
|
326
|
+
span: entries.length
|
|
327
|
+
? {
|
|
328
|
+
from: entries[0].timestamp,
|
|
329
|
+
to: entries[entries.length - 1].timestamp,
|
|
330
|
+
}
|
|
331
|
+
: undefined,
|
|
332
|
+
eventTypes: Object.keys(events).length ? events : undefined,
|
|
333
|
+
...(failedRegions?.length
|
|
334
|
+
? {
|
|
335
|
+
warning: 'The log-stream query failed in region '
|
|
336
|
+
+ `${failedRegions.join(', ')}, so this result is incomplete.`,
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
339
|
+
...(entries.length
|
|
340
|
+
? {}
|
|
341
|
+
: {
|
|
342
|
+
hint: 'No entries matched. Check these causes in order: the '
|
|
343
|
+
+ 'search window defaults to the last 3 days, so pass startTime '
|
|
344
|
+
+ 'and endTime for an older session; the backend deletes entries '
|
|
345
|
+
+ 'after 30 days; the backend picks which regions to query from '
|
|
346
|
+
+ 'the analytics table, so a session that never recorded an '
|
|
347
|
+
+ 'analytics event reads as empty here even when entries exist; '
|
|
348
|
+
+ 'or the filters are too narrow. Cross-check with '
|
|
349
|
+
+ 'session_analytics_logs.',
|
|
350
|
+
}),
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Fields that are identical on every entry, hoisted out so they are not
|
|
355
|
+
* repeated hundreds of times. */
|
|
356
|
+
function contextOf(entries: SessionLogRow[]) {
|
|
357
|
+
const uniq = (pick: (entry: SessionLogRow) => string | undefined) => {
|
|
358
|
+
const set = new Set(entries.map(pick).filter(Boolean) as string[])
|
|
359
|
+
return [...set]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const one = (values: string[]) => (values.length === 1 ? values[0] : values)
|
|
363
|
+
return entries.length
|
|
364
|
+
? {
|
|
365
|
+
appId: one(uniq(e => e.app_id)),
|
|
366
|
+
providerId: one(uniq(e => e.provider_id)),
|
|
367
|
+
deviceId: one(uniq(e => e.device_id)),
|
|
368
|
+
source: one(uniq(e => e.source)),
|
|
369
|
+
}
|
|
370
|
+
: undefined
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function compact(entries: SessionLogRow[]): CompactEntry[] {
|
|
374
|
+
const devices = new Set(entries.map(e => e.device_id))
|
|
375
|
+
const sources = new Set(entries.map(e => e.source))
|
|
376
|
+
return entries.map(e => ({
|
|
377
|
+
ts: e.timestamp,
|
|
378
|
+
level: e.log_level,
|
|
379
|
+
...(e.event_type ? { event: e.event_type } : {}),
|
|
380
|
+
logger: e.log_type,
|
|
381
|
+
line: e.log_line,
|
|
382
|
+
// Only worth repeating per entry once the page actually mixes them.
|
|
383
|
+
...(devices.size > 1 ? { device: e.device_id } : {}),
|
|
384
|
+
...(sources.size > 1 ? { source: e.source } : {}),
|
|
385
|
+
...(e.metadata && e.metadata !== '{}' ? { metadata: e.metadata } : {}),
|
|
386
|
+
}))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function hasEvent(entry: SessionLogRow): boolean {
|
|
390
|
+
return !!entry.event_type
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function clampLimit(limit: number): number {
|
|
394
|
+
return Math.min(Math.max(Math.trunc(limit), 1), MAX_PAGE)
|
|
395
|
+
}
|