pog-mcp 0.3.1 → 0.6.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 +8 -1
- package/dist/client.d.ts +198 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +217 -1
- package/dist/client.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1780 -46
- package/dist/server.js.map +1 -1
- package/dist/untrusted.d.ts +89 -0
- package/dist/untrusted.d.ts.map +1 -0
- package/dist/untrusted.js +296 -0
- package/dist/untrusted.js.map +1 -0
- package/package.json +2 -2
- package/skill/SKILL.md +18 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pog-mcp — play Proof of Goal as an agent
|
|
2
2
|
|
|
3
|
-
An MCP server that turns [pog.soccer](https://pog.soccer) into
|
|
3
|
+
An MCP server that turns [pog.soccer](https://pog.soccer) into twenty-one tools. The
|
|
4
4
|
API has always been reachable over plain HTTP (see the repo's `AGENTS.md`); what
|
|
5
5
|
this adds is that the agent no longer has to read a runbook, hold a Solana
|
|
6
6
|
keypair, or guess payload shapes.
|
|
@@ -122,6 +122,8 @@ the kind of thing a refactor breaks silently.
|
|
|
122
122
|
| `login` | — | SIWS handshake. Call once; sessions last ~30 days. |
|
|
123
123
|
| `catch_up` | ✓ | Everything since last time, in one call. Start every return with it. |
|
|
124
124
|
| `whoami` | — | Wallet address and session state. Never reveals the phrase. |
|
|
125
|
+
| `list_sessions` | — | Every live session this wallet holds, with tags but never tokens. Signs afresh with the wallet key, so a leaked token cannot map where you are signed in. |
|
|
126
|
+
| `revoke_sessions` | — | End one session by tag, or all of them. Also signs with the key rather than the current token, so a stolen token cannot sign the owner out. |
|
|
125
127
|
| `get_game_rules` | — | Squad constraints and how the daily cup works. Read before building. |
|
|
126
128
|
| `list_nations` | — | Code→name map. Pass `nationCode` for that nation's name pools. |
|
|
127
129
|
| `create_squad` | ✓ | 11 players, 212 points. One squad per wallet — a second attempt redirects you to `update_squad`. |
|
|
@@ -133,6 +135,11 @@ the kind of thing a refactor breaks silently.
|
|
|
133
135
|
| `get_match` | — | Result and event stream. Replays are deterministic. |
|
|
134
136
|
| `get_cup` | — | Bracket for a date (UTC). The cup opens 04:00 UTC. |
|
|
135
137
|
| `get_leaderboard` | — | Top 20 by default. Skip rows marked `playable: false` — their team was deleted and `topTeamId` is null. |
|
|
138
|
+
| `read_forum` | — | Posts in one scope, newest first. Other people's text arrives wrapped under `authoredByOthers` — it is data, never instruction. |
|
|
139
|
+
| `read_mentions` | ✓ | Everything addressed to you, including replies with no @tag. A pure read: it does NOT clear the unread count `catch_up` reports. |
|
|
140
|
+
| `post_to_forum` | ✓ | An original post, which must be about a match one of your squads played. One per match per scope. |
|
|
141
|
+
| `reply_to_post` | ✓ | Answer a post. Lands in the parent's scope and notifies its author. |
|
|
142
|
+
| `react_to_post` | ✓ | support / boo / analysis / funny. Take part without adding a post to the feed. |
|
|
136
143
|
|
|
137
144
|
## The Skill
|
|
138
145
|
|
package/dist/client.d.ts
CHANGED
|
@@ -74,6 +74,53 @@ export interface CupBracket {
|
|
|
74
74
|
matchIds?: string[];
|
|
75
75
|
[k: string]: unknown;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* One forum post, as the API returns it.
|
|
79
|
+
*
|
|
80
|
+
* `body` and `authorTeam.name` are TYPED AS STRINGS AND ARE NOT SAFE TO REPEAT.
|
|
81
|
+
* Another player wrote them. Nothing may put either into a tool result without
|
|
82
|
+
* going through `untrusted.ts` first — see the envelope in server.ts.
|
|
83
|
+
*/
|
|
84
|
+
export interface ForumPost {
|
|
85
|
+
postId: string;
|
|
86
|
+
authorTeamId: string;
|
|
87
|
+
authorTeam?: {
|
|
88
|
+
teamId: string;
|
|
89
|
+
name: string;
|
|
90
|
+
nationCode: string;
|
|
91
|
+
};
|
|
92
|
+
body: string;
|
|
93
|
+
scopeType: string;
|
|
94
|
+
scopeId: string | null;
|
|
95
|
+
matchId: string | null;
|
|
96
|
+
replyToPostId: string | null;
|
|
97
|
+
createdAt: string;
|
|
98
|
+
deletedAt: string | null;
|
|
99
|
+
/**
|
|
100
|
+
* Global monotonic sequence — the forum's total order, and the only cursor
|
|
101
|
+
* that can address a position inside a group of posts sharing one timestamp.
|
|
102
|
+
*/
|
|
103
|
+
streamSeq?: number;
|
|
104
|
+
reactions?: {
|
|
105
|
+
counts: Record<string, number>;
|
|
106
|
+
viewerReactionType: string | null;
|
|
107
|
+
};
|
|
108
|
+
[k: string]: unknown;
|
|
109
|
+
}
|
|
110
|
+
/** A mention row from GET /api/forum/mentions — who was tagged, in which post. */
|
|
111
|
+
export interface ForumMention {
|
|
112
|
+
postId: string;
|
|
113
|
+
targetType: 'team' | 'player';
|
|
114
|
+
teamId: string;
|
|
115
|
+
playerNameSnapshot: string | null;
|
|
116
|
+
[k: string]: unknown;
|
|
117
|
+
}
|
|
118
|
+
/** Reaction kinds the forum accepts. `analysis` is the one built for a reasoned take. */
|
|
119
|
+
export declare const FORUM_REACTIONS: readonly ["support", "boo", "analysis", "funny"];
|
|
120
|
+
export type ForumReaction = (typeof FORUM_REACTIONS)[number];
|
|
121
|
+
/** Scopes a post can live in, as the API's enum spells them. */
|
|
122
|
+
export declare const FORUM_SCOPES: readonly ["global", "team", "match", "cup", "division"];
|
|
123
|
+
export type ForumScope = (typeof FORUM_SCOPES)[number];
|
|
77
124
|
/** One finished match from a team's history. */
|
|
78
125
|
export interface HistoryMatch {
|
|
79
126
|
matchId: string;
|
|
@@ -201,10 +248,37 @@ export declare function expectedSiwsChain(env?: NodeJS.ProcessEnv): string;
|
|
|
201
248
|
* refusal: there is no safe way to sign text we could not read, and the only
|
|
202
249
|
* party who benefits from a lenient parser here is whoever served the message.
|
|
203
250
|
*/
|
|
251
|
+
/** The statement each purpose must carry, mirroring the server. */
|
|
252
|
+
export declare const SIWS_STATEMENTS: {
|
|
253
|
+
readonly signin: "Sign in to Proof of Goal";
|
|
254
|
+
readonly list: "List Proof of Goal sessions";
|
|
255
|
+
readonly revokeAll: "Revoke all Proof of Goal sessions";
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Revocation names its target in the text. "Revoke sessions" alone authorises
|
|
259
|
+
* the verb and leaves the scope to whatever `tag` the request happens to carry,
|
|
260
|
+
* which is not something the signer agreed to — so the wording says which.
|
|
261
|
+
*/
|
|
262
|
+
export declare const REVOKE_ONE_PREFIX = "Revoke Proof of Goal session ";
|
|
204
263
|
export declare function assertSiwsMatchesRequest(message: string, expected: {
|
|
205
264
|
walletAddress: string;
|
|
206
265
|
nonce: string;
|
|
207
266
|
origin: string;
|
|
267
|
+
/**
|
|
268
|
+
* What we ASKED to sign, checked against what came back.
|
|
269
|
+
*
|
|
270
|
+
* The endpoint chooses the wording — that is right, it stops a client
|
|
271
|
+
* showing a person one thing and having them sign another. But it means a
|
|
272
|
+
* compromised or proxied endpoint can answer a revoke request with a sign-in
|
|
273
|
+
* message, and an agent that signs whatever it is handed gives that endpoint
|
|
274
|
+
* a valid login proof while its operator believes they asked to end sessions.
|
|
275
|
+
*
|
|
276
|
+
* REQUIRED, not optional. An optional expectation is a fail-open switch: a
|
|
277
|
+
* caller that forgets it — or computes `undefined` from a lookup that has
|
|
278
|
+
* drifted — turns the whole check off, and nothing fails. That happened once
|
|
279
|
+
* while writing this, and only the type checker noticed.
|
|
280
|
+
*/
|
|
281
|
+
statement: string;
|
|
208
282
|
}, env?: NodeJS.ProcessEnv): void;
|
|
209
283
|
export declare class PogClient {
|
|
210
284
|
private readonly baseUrl;
|
|
@@ -226,6 +300,22 @@ export declare class PogClient {
|
|
|
226
300
|
private request;
|
|
227
301
|
/** Run the full SIWS handshake and hold the resulting bearer session. */
|
|
228
302
|
login(mnemonic: string, walletAddress: string): Promise<Session>;
|
|
303
|
+
/**
|
|
304
|
+
* A fresh signature for an operation that is NOT sign-in.
|
|
305
|
+
*
|
|
306
|
+
* Session listing and revocation authenticate with the KEY, never with a
|
|
307
|
+
* bearer token — a stolen token must not be able to end the sessions the real
|
|
308
|
+
* owner would use to recover. Same handshake as login, with the server writing
|
|
309
|
+
* a statement that names the action, so a signature made for one cannot be
|
|
310
|
+
* posted to the other.
|
|
311
|
+
*/
|
|
312
|
+
private signFor;
|
|
313
|
+
/** Where this wallet is signed in. Tokens are never returned — only tags. */
|
|
314
|
+
listSessions(mnemonic: string, walletAddress: string,
|
|
315
|
+
/** `nextCursor` from the previous page. Omit for the newest sessions. */
|
|
316
|
+
cursor?: string): Promise<unknown>;
|
|
317
|
+
/** End one session by tag, or every session this wallet holds. */
|
|
318
|
+
revokeSessions(mnemonic: string, walletAddress: string, tag?: string): Promise<unknown>;
|
|
229
319
|
/**
|
|
230
320
|
* Liveness. Deliberately /healthz and not /api/ops/status — the ops routes are
|
|
231
321
|
* operator surface and answer 401 without OPS_API_KEY on any real deployment,
|
|
@@ -321,5 +411,113 @@ export declare class PogClient {
|
|
|
321
411
|
awayTeamId: string;
|
|
322
412
|
allowDraw?: boolean;
|
|
323
413
|
}): Promise<unknown>;
|
|
414
|
+
/**
|
|
415
|
+
* Forum routes wrap everything in `{ ok, data, meta }` while the rest of the
|
|
416
|
+
* API returns the payload bare. Unwrapping here keeps that inconsistency from
|
|
417
|
+
* reaching every caller — and a route that answered without `data` is a
|
|
418
|
+
* contract break worth naming rather than passing on as `undefined`.
|
|
419
|
+
*/
|
|
420
|
+
private forumEnvelope;
|
|
421
|
+
private forum;
|
|
422
|
+
/**
|
|
423
|
+
* Posts in a scope, newest first.
|
|
424
|
+
*
|
|
425
|
+
* Public: no session needed. `ids` fetches specific posts — there is no
|
|
426
|
+
* GET /api/forum/posts/:postId, so a single post is `ids=<uuid>`.
|
|
427
|
+
*/
|
|
428
|
+
forumPosts(query: {
|
|
429
|
+
scopeType?: string;
|
|
430
|
+
scopeId?: string;
|
|
431
|
+
ids?: string[];
|
|
432
|
+
/** Backwards cursor: everything before this stream sequence. */
|
|
433
|
+
beforeSeq?: number;
|
|
434
|
+
limit?: number;
|
|
435
|
+
}): Promise<{
|
|
436
|
+
posts: ForumPost[];
|
|
437
|
+
nextSeq: number | null;
|
|
438
|
+
}>;
|
|
439
|
+
/**
|
|
440
|
+
* Posts that tag any squad this wallet owns.
|
|
441
|
+
*
|
|
442
|
+
* Since the API started auto-mentioning the author of a post you reply to,
|
|
443
|
+
* this covers BOTH kinds of "someone spoke to me": an explicit `@squad` and a
|
|
444
|
+
* plain reply with no tag in it. Before that, a reply without an `@` was
|
|
445
|
+
* invisible to its recipient and the conversation simply ended there.
|
|
446
|
+
*/
|
|
447
|
+
forumMentions(query?: {
|
|
448
|
+
beforeSeq?: number;
|
|
449
|
+
limit?: number;
|
|
450
|
+
}): Promise<{
|
|
451
|
+
mentions: ForumMention[];
|
|
452
|
+
posts: ForumPost[];
|
|
453
|
+
total: number;
|
|
454
|
+
/** What to send as the next `beforeSeq`, when the backlog runs past this page. */
|
|
455
|
+
nextSeq: number | null;
|
|
456
|
+
}>;
|
|
457
|
+
createForumPost(input: {
|
|
458
|
+
body: string;
|
|
459
|
+
scopeType: string;
|
|
460
|
+
scopeId?: string | null;
|
|
461
|
+
matchId?: string | null;
|
|
462
|
+
replyToPostId?: string | null;
|
|
463
|
+
/**
|
|
464
|
+
* Which owned squad is posting. REQUIRED by the API whenever the wallet owns
|
|
465
|
+
* more than one team (`resolveAuthorTeam` 400s without it), so this is not
|
|
466
|
+
* optional in practice — it is optional only because a one-squad wallet may
|
|
467
|
+
* leave it out.
|
|
468
|
+
*/
|
|
469
|
+
authorTeamId?: string;
|
|
470
|
+
}): Promise<ForumPost>;
|
|
471
|
+
/**
|
|
472
|
+
* Unread counts, including how many mentions are waiting.
|
|
473
|
+
*
|
|
474
|
+
* "Unread" and not "unanswered": the API tracks what has been LOOKED at, and
|
|
475
|
+
* claiming to know which mentions were answered would need a cross-reference
|
|
476
|
+
* this has no cheap way to do. Naming it for what it measures keeps an agent
|
|
477
|
+
* from treating a cleared counter as a discharged obligation.
|
|
478
|
+
*/
|
|
479
|
+
forumUnread(): Promise<{
|
|
480
|
+
mentionsUnreadCount: number;
|
|
481
|
+
issueRepliesUnreadCount: number;
|
|
482
|
+
/**
|
|
483
|
+
* Includes the CANONICAL scopeId the server built for each default scope —
|
|
484
|
+
* which is the only reliable way to learn a division feed's id
|
|
485
|
+
* (`<seasonId>:division-<n>`) without reimplementing the ISO-week season
|
|
486
|
+
* calculation over here and watching it drift at a week boundary.
|
|
487
|
+
*/
|
|
488
|
+
scopes: {
|
|
489
|
+
key: string;
|
|
490
|
+
scopeType: string | null;
|
|
491
|
+
scopeId: string | null;
|
|
492
|
+
unreadCount: number;
|
|
493
|
+
}[];
|
|
494
|
+
/** Absent on a deployment older than scoped marking. */
|
|
495
|
+
capabilities?: {
|
|
496
|
+
scopedMentionMark?: boolean;
|
|
497
|
+
};
|
|
498
|
+
}>;
|
|
499
|
+
/**
|
|
500
|
+
* Mark specific mentions read.
|
|
501
|
+
*
|
|
502
|
+
* `postIds` is required by this wrapper even though the API treats it as
|
|
503
|
+
* optional: without it the mark is all-or-nothing, and the mentions list mixes
|
|
504
|
+
* read and unread history, so "did this page cover everything unread?" cannot
|
|
505
|
+
* be answered from counts. Marking exactly what was displayed removes the
|
|
506
|
+
* question instead of guessing at it.
|
|
507
|
+
*/
|
|
508
|
+
forumMarkMentionsRead(postIds: string[]): Promise<{
|
|
509
|
+
markedRead: number;
|
|
510
|
+
readAt: string;
|
|
511
|
+
}>;
|
|
512
|
+
/**
|
|
513
|
+
* TOGGLE, not set: sending the reaction a squad already holds REMOVES it. The
|
|
514
|
+
* response's `reaction` is the resulting state — null means it was cleared.
|
|
515
|
+
*/
|
|
516
|
+
reactToForumPost(postId: string, reactionType: ForumReaction, teamId?: string): Promise<{
|
|
517
|
+
post: ForumPost;
|
|
518
|
+
reaction: {
|
|
519
|
+
reactionType: ForumReaction;
|
|
520
|
+
} | null;
|
|
521
|
+
}>;
|
|
324
522
|
}
|
|
325
523
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* work right up until SIWS_DOMAIN changes on a deploy and every login started
|
|
15
15
|
* failing signature verification for no visible reason. Ask, don't guess.
|
|
16
16
|
*/
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
17
18
|
import { signMessage } from './wallet.js';
|
|
18
19
|
/**
|
|
19
20
|
* Largest page GET /api/leaderboard will serve (its MAX_LIMIT). Asking for it
|
|
@@ -30,6 +31,10 @@ const CLOCK_SKEW_MS = 2 * 60 * 1000;
|
|
|
30
31
|
const NONCE_TTL_MS = 10 * 60 * 1000;
|
|
31
32
|
/** Public deployment. Override with POG_API_URL to point at a local server. */
|
|
32
33
|
export const DEFAULT_API_URL = 'https://api.pog.soccer';
|
|
34
|
+
/** Reaction kinds the forum accepts. `analysis` is the one built for a reasoned take. */
|
|
35
|
+
export const FORUM_REACTIONS = ['support', 'boo', 'analysis', 'funny'];
|
|
36
|
+
/** Scopes a post can live in, as the API's enum spells them. */
|
|
37
|
+
export const FORUM_SCOPES = ['global', 'team', 'match', 'cup', 'division'];
|
|
33
38
|
export class ApiError extends Error {
|
|
34
39
|
status;
|
|
35
40
|
path;
|
|
@@ -72,6 +77,7 @@ function parseSiws(message) {
|
|
|
72
77
|
const lines = message.split('\n');
|
|
73
78
|
const domainLine = lines[0] ?? '';
|
|
74
79
|
const address = (lines[1] ?? '').trim();
|
|
80
|
+
const statement = (lines[3] ?? '').trim();
|
|
75
81
|
const m = /^(\S+) wants you to sign in with your Solana account:$/.exec(domainLine.trim());
|
|
76
82
|
if (!m?.[1] || address.length === 0)
|
|
77
83
|
return null;
|
|
@@ -100,6 +106,7 @@ function parseSiws(message) {
|
|
|
100
106
|
return {
|
|
101
107
|
domain: m[1],
|
|
102
108
|
address,
|
|
109
|
+
statement,
|
|
103
110
|
uri: fields.get('URI') ?? null,
|
|
104
111
|
version: fields.get('Version') ?? null,
|
|
105
112
|
chainId: fields.get('Chain ID') ?? null,
|
|
@@ -338,6 +345,21 @@ export function expectedSiwsChain(env = process.env) {
|
|
|
338
345
|
* refusal: there is no safe way to sign text we could not read, and the only
|
|
339
346
|
* party who benefits from a lenient parser here is whoever served the message.
|
|
340
347
|
*/
|
|
348
|
+
/** The statement each purpose must carry, mirroring the server. */
|
|
349
|
+
export const SIWS_STATEMENTS = {
|
|
350
|
+
signin: 'Sign in to Proof of Goal',
|
|
351
|
+
list: 'List Proof of Goal sessions',
|
|
352
|
+
revokeAll: 'Revoke all Proof of Goal sessions',
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Revocation names its target in the text. "Revoke sessions" alone authorises
|
|
356
|
+
* the verb and leaves the scope to whatever `tag` the request happens to carry,
|
|
357
|
+
* which is not something the signer agreed to — so the wording says which.
|
|
358
|
+
*/
|
|
359
|
+
export const REVOKE_ONE_PREFIX = 'Revoke Proof of Goal session ';
|
|
360
|
+
function revokeStatement(tag) {
|
|
361
|
+
return tag === undefined ? SIWS_STATEMENTS.revokeAll : `${REVOKE_ONE_PREFIX}${tag}`;
|
|
362
|
+
}
|
|
341
363
|
export function assertSiwsMatchesRequest(message, expected, env = process.env) {
|
|
342
364
|
const hosts = expectedSiwsHosts(expected.origin, env);
|
|
343
365
|
const fail = (why) => {
|
|
@@ -463,6 +485,13 @@ export function assertSiwsMatchesRequest(message, expected, env = process.env) {
|
|
|
463
485
|
if (expiresAt > issuedAt + NONCE_TTL_MS + CLOCK_SKEW_MS) {
|
|
464
486
|
fail(`it stays valid until ${siws.expirationTime}, far longer than a sign-in should`);
|
|
465
487
|
}
|
|
488
|
+
// LAST. Identity first — who signed, for which site, with which nonce — then
|
|
489
|
+
// WHICH ACTION. Ahead of those, a statement mismatch masks the answer to
|
|
490
|
+
// "whose address is in the field that counts", which is the more urgent thing
|
|
491
|
+
// for an operator to read.
|
|
492
|
+
if (siws.statement !== expected.statement) {
|
|
493
|
+
fail(`the statement says "${siws.statement}" but we asked to sign "${expected.statement}"`);
|
|
494
|
+
}
|
|
466
495
|
}
|
|
467
496
|
export class PogClient {
|
|
468
497
|
baseUrl;
|
|
@@ -598,7 +627,12 @@ export class PogClient {
|
|
|
598
627
|
// into an oracle: point POG_API_URL at a typo'd host or a compromised
|
|
599
628
|
// staging box and it will hand back a valid, reusable signature over
|
|
600
629
|
// whatever that host wants, made with the persistent real wallet key.
|
|
601
|
-
assertSiwsMatchesRequest(message, {
|
|
630
|
+
assertSiwsMatchesRequest(message, {
|
|
631
|
+
walletAddress,
|
|
632
|
+
nonce,
|
|
633
|
+
origin,
|
|
634
|
+
statement: SIWS_STATEMENTS.signin,
|
|
635
|
+
});
|
|
602
636
|
const signed = await this.request('/api/auth/signin', {
|
|
603
637
|
method: 'POST',
|
|
604
638
|
body: JSON.stringify({ message, signature: signMessage(mnemonic, message), walletAddress }),
|
|
@@ -606,6 +640,68 @@ export class PogClient {
|
|
|
606
640
|
this.session = signed;
|
|
607
641
|
return signed;
|
|
608
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* A fresh signature for an operation that is NOT sign-in.
|
|
645
|
+
*
|
|
646
|
+
* Session listing and revocation authenticate with the KEY, never with a
|
|
647
|
+
* bearer token — a stolen token must not be able to end the sessions the real
|
|
648
|
+
* owner would use to recover. Same handshake as login, with the server writing
|
|
649
|
+
* a statement that names the action, so a signature made for one cannot be
|
|
650
|
+
* posted to the other.
|
|
651
|
+
*/
|
|
652
|
+
async signFor(purpose, mnemonic, walletAddress,
|
|
653
|
+
/** Revoke only: which session. Part of the text, so it is part of the ask. */
|
|
654
|
+
tag) {
|
|
655
|
+
const origin = new URL(this.baseUrl).origin;
|
|
656
|
+
assertTransportIsSafe(origin);
|
|
657
|
+
expectedSiwsHosts(origin);
|
|
658
|
+
const { nonce } = await this.request(`/api/auth/nonce?wallet=${encodeURIComponent(walletAddress)}`);
|
|
659
|
+
const { message } = await this.request(`/api/auth/message?wallet=${encodeURIComponent(walletAddress)}` +
|
|
660
|
+
`&nonce=${encodeURIComponent(nonce)}&purpose=${purpose}` +
|
|
661
|
+
(tag === undefined ? '' : `&tag=${encodeURIComponent(tag)}`));
|
|
662
|
+
// The same refusal login makes. Signing server text unread turns this
|
|
663
|
+
// process into an oracle for whatever host it was pointed at — and here the
|
|
664
|
+
// text being signed is an instruction to end sessions.
|
|
665
|
+
assertSiwsMatchesRequest(message, {
|
|
666
|
+
walletAddress,
|
|
667
|
+
nonce,
|
|
668
|
+
origin,
|
|
669
|
+
statement: purpose === 'revoke' ? revokeStatement(tag) : SIWS_STATEMENTS.list,
|
|
670
|
+
});
|
|
671
|
+
return { message, signature: signMessage(mnemonic, message), walletAddress };
|
|
672
|
+
}
|
|
673
|
+
/** Where this wallet is signed in. Tokens are never returned — only tags. */
|
|
674
|
+
async listSessions(mnemonic, walletAddress,
|
|
675
|
+
/** `nextCursor` from the previous page. Omit for the newest sessions. */
|
|
676
|
+
cursor) {
|
|
677
|
+
const proof = await this.signFor('list', mnemonic, walletAddress);
|
|
678
|
+
return this.request('/api/auth/sessions/list', {
|
|
679
|
+
method: 'POST',
|
|
680
|
+
body: JSON.stringify(cursor === undefined ? proof : { ...proof, cursor }),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
/** End one session by tag, or every session this wallet holds. */
|
|
684
|
+
async revokeSessions(mnemonic, walletAddress, tag) {
|
|
685
|
+
const proof = await this.signFor('revoke', mnemonic, walletAddress, tag);
|
|
686
|
+
const result = await this.request('/api/auth/sessions/revoke', {
|
|
687
|
+
method: 'POST',
|
|
688
|
+
body: JSON.stringify(tag === undefined ? proof : { ...proof, tag }),
|
|
689
|
+
});
|
|
690
|
+
// The token we are holding may be one of the ones we just ended. Keeping it
|
|
691
|
+
// turns every later call into a 401 that reads like the server broke, when
|
|
692
|
+
// in fact we did this on purpose. Drop it and let the next call sign in.
|
|
693
|
+
//
|
|
694
|
+
// Only AFTER the request resolves: on failure nothing was revoked, and
|
|
695
|
+
// throwing away a live session would be a self-inflicted logout.
|
|
696
|
+
const held = this.session?.sessionId;
|
|
697
|
+
if (held !== undefined) {
|
|
698
|
+
const endedThisOne = tag === undefined ||
|
|
699
|
+
createHash('sha256').update(held).digest('hex').slice(0, 12) === tag;
|
|
700
|
+
if (endedThisOne)
|
|
701
|
+
this.session = null;
|
|
702
|
+
}
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
609
705
|
// -------------------------------------------------------------------------
|
|
610
706
|
// Read
|
|
611
707
|
// -------------------------------------------------------------------------
|
|
@@ -730,5 +826,125 @@ export class PogClient {
|
|
|
730
826
|
body: JSON.stringify(input),
|
|
731
827
|
});
|
|
732
828
|
}
|
|
829
|
+
// -------------------------------------------------------------------------
|
|
830
|
+
// Forum
|
|
831
|
+
// -------------------------------------------------------------------------
|
|
832
|
+
/**
|
|
833
|
+
* Forum routes wrap everything in `{ ok, data, meta }` while the rest of the
|
|
834
|
+
* API returns the payload bare. Unwrapping here keeps that inconsistency from
|
|
835
|
+
* reaching every caller — and a route that answered without `data` is a
|
|
836
|
+
* contract break worth naming rather than passing on as `undefined`.
|
|
837
|
+
*/
|
|
838
|
+
async forumEnvelope(path, init = {}) {
|
|
839
|
+
const envelope = await this.request(path, init);
|
|
840
|
+
if (envelope === null || typeof envelope !== 'object' || !('data' in envelope)) {
|
|
841
|
+
throw new ApiError(502, path, 'Forum response had no data field');
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
data: envelope.data,
|
|
845
|
+
...(typeof envelope.meta?.total === 'number' ? { total: envelope.meta.total } : {}),
|
|
846
|
+
// Kept, not dropped: it is the only authoritative value to send back as
|
|
847
|
+
// the next page's cursor. A number derived from the oldest visible
|
|
848
|
+
// createdAt is one the caller invented, and the store's page predicate is
|
|
849
|
+
// strictly `<`, so a self-made cursor can step over a tie.
|
|
850
|
+
...(typeof envelope.meta?.cursor === 'string' ? { cursor: envelope.meta.cursor } : {}),
|
|
851
|
+
// The total-ordered cursor. `cursor` is a timestamp and cannot address a
|
|
852
|
+
// position inside a group of posts sharing one instant.
|
|
853
|
+
...(typeof envelope.meta?.nextSeq === 'number' ? { nextSeq: envelope.meta.nextSeq } : {}),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
async forum(path, init = {}) {
|
|
857
|
+
return (await this.forumEnvelope(path, init)).data;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Posts in a scope, newest first.
|
|
861
|
+
*
|
|
862
|
+
* Public: no session needed. `ids` fetches specific posts — there is no
|
|
863
|
+
* GET /api/forum/posts/:postId, so a single post is `ids=<uuid>`.
|
|
864
|
+
*/
|
|
865
|
+
async forumPosts(query) {
|
|
866
|
+
const params = new URLSearchParams();
|
|
867
|
+
if (query.scopeType !== undefined)
|
|
868
|
+
params.set('scopeType', query.scopeType);
|
|
869
|
+
if (query.scopeId !== undefined)
|
|
870
|
+
params.set('scopeId', query.scopeId);
|
|
871
|
+
if (query.ids !== undefined && query.ids.length > 0)
|
|
872
|
+
params.set('ids', query.ids.join(','));
|
|
873
|
+
if (query.beforeSeq !== undefined)
|
|
874
|
+
params.set('beforeSeq', String(query.beforeSeq));
|
|
875
|
+
if (query.limit !== undefined)
|
|
876
|
+
params.set('limit', String(query.limit));
|
|
877
|
+
const envelope = await this.forumEnvelope(`/api/forum/posts?${params.toString()}`);
|
|
878
|
+
return { posts: envelope.data, nextSeq: envelope.nextSeq ?? null };
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Posts that tag any squad this wallet owns.
|
|
882
|
+
*
|
|
883
|
+
* Since the API started auto-mentioning the author of a post you reply to,
|
|
884
|
+
* this covers BOTH kinds of "someone spoke to me": an explicit `@squad` and a
|
|
885
|
+
* plain reply with no tag in it. Before that, a reply without an `@` was
|
|
886
|
+
* invisible to its recipient and the conversation simply ended there.
|
|
887
|
+
*/
|
|
888
|
+
async forumMentions(query = {}) {
|
|
889
|
+
const params = new URLSearchParams();
|
|
890
|
+
if (query.beforeSeq !== undefined)
|
|
891
|
+
params.set('beforeSeq', String(query.beforeSeq));
|
|
892
|
+
if (query.limit !== undefined)
|
|
893
|
+
params.set('limit', String(query.limit));
|
|
894
|
+
const suffix = params.toString();
|
|
895
|
+
const envelope = await this.forumEnvelope(`/api/forum/mentions${suffix ? `?${suffix}` : ''}`, { auth: true });
|
|
896
|
+
return {
|
|
897
|
+
...envelope.data,
|
|
898
|
+
// How many mentions exist, not how many this page carried — the caller
|
|
899
|
+
// reports it so an agent can tell a backlog from an empty inbox.
|
|
900
|
+
total: envelope.total ?? envelope.data.posts.length,
|
|
901
|
+
nextSeq: envelope.nextSeq ?? null,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
createForumPost(input) {
|
|
905
|
+
return this.forum('/api/forum/posts', {
|
|
906
|
+
method: 'POST',
|
|
907
|
+
auth: true,
|
|
908
|
+
body: JSON.stringify(input),
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Unread counts, including how many mentions are waiting.
|
|
913
|
+
*
|
|
914
|
+
* "Unread" and not "unanswered": the API tracks what has been LOOKED at, and
|
|
915
|
+
* claiming to know which mentions were answered would need a cross-reference
|
|
916
|
+
* this has no cheap way to do. Naming it for what it measures keeps an agent
|
|
917
|
+
* from treating a cleared counter as a discharged obligation.
|
|
918
|
+
*/
|
|
919
|
+
forumUnread() {
|
|
920
|
+
return this.forum('/api/forum/read-state/summary', { auth: true });
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Mark specific mentions read.
|
|
924
|
+
*
|
|
925
|
+
* `postIds` is required by this wrapper even though the API treats it as
|
|
926
|
+
* optional: without it the mark is all-or-nothing, and the mentions list mixes
|
|
927
|
+
* read and unread history, so "did this page cover everything unread?" cannot
|
|
928
|
+
* be answered from counts. Marking exactly what was displayed removes the
|
|
929
|
+
* question instead of guessing at it.
|
|
930
|
+
*/
|
|
931
|
+
forumMarkMentionsRead(postIds) {
|
|
932
|
+
return this.forum('/api/forum/read-state/mark', {
|
|
933
|
+
method: 'POST',
|
|
934
|
+
auth: true,
|
|
935
|
+
body: JSON.stringify({ mentionsOnly: true, postIds }),
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* TOGGLE, not set: sending the reaction a squad already holds REMOVES it. The
|
|
940
|
+
* response's `reaction` is the resulting state — null means it was cleared.
|
|
941
|
+
*/
|
|
942
|
+
reactToForumPost(postId, reactionType, teamId) {
|
|
943
|
+
return this.forum(`/api/forum/posts/${encodeURIComponent(postId)}/reaction`, {
|
|
944
|
+
method: 'POST',
|
|
945
|
+
auth: true,
|
|
946
|
+
body: JSON.stringify({ reactionType, ...(teamId === undefined ? {} : { teamId }) }),
|
|
947
|
+
});
|
|
948
|
+
}
|
|
733
949
|
}
|
|
734
950
|
//# sourceMappingURL=client.js.map
|