@dopamint-fun/open-sdk 0.2.0-dev.0 → 0.2.0-dev.10
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 +3 -3
- package/dist/agentHttp.js +12 -15
- package/dist/bytes.js +1 -1
- package/dist/claim.js +38 -22
- package/dist/cli.js +79 -56
- package/dist/identity.js +19 -37
- package/dist/index.d.ts +7 -7
- package/dist/index.js +7 -7
- package/dist/offer.js +3 -3
- package/dist/openTournament.d.ts +22 -0
- package/dist/openTournament.js +34 -3
- package/dist/refusal.d.ts +5 -0
- package/dist/refusal.js +21 -0
- package/dist/room.d.ts +29 -7
- package/dist/room.js +40 -10
- package/dist/seatState.d.ts +61 -5
- package/dist/seatState.js +162 -7
- package/dist/seatTurn.d.ts +15 -2
- package/dist/seatTurn.js +99 -22
- package/dist/session.d.ts +12 -0
- package/dist/session.js +153 -55
- package/dist/sessionCodec.d.ts +162 -2
- package/dist/sessionCodec.js +658 -20
- package/dist/sessionWire.d.ts +6 -5
- package/dist/sessionWire.js +8 -7
- package/dist/settlement.d.ts +5 -1
- package/dist/settlement.js +6 -7
- package/dist/tour.js +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `libs/dopa-open
|
|
1
|
+
# `libs/dopa-open/client-ts`
|
|
2
2
|
|
|
3
3
|
Status: implemented
|
|
4
4
|
Ownership: Optional official TypeScript SDK for DOPA-OPEN self-custody agents:
|
|
@@ -36,8 +36,8 @@ signatures, not whether this package is used. Allowed dependencies:
|
|
|
36
36
|
## Parity
|
|
37
37
|
|
|
38
38
|
Preimages and signatures are pinned byte-identical to the Rust signer by
|
|
39
|
-
`libs/dopa-open
|
|
40
|
-
`libs/dopa-open
|
|
39
|
+
`libs/dopa-open/client-rs/vectors/ts-signer-parity.json`, generated by
|
|
40
|
+
`libs/dopa-open/client-rs/tests/ts_parity_vectors.rs` and replayed by
|
|
41
41
|
`src/parity.test.ts` here. Regenerate with
|
|
42
42
|
`WRITE_TS_PARITY_VECTORS=1 cargo test -p dopa-open-client-rs --test
|
|
43
43
|
ts_parity_vectors`; a wire change updates both sides together.
|
package/dist/agentHttp.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* tour or table request without ever holding a bearer credential.
|
|
3
3
|
*
|
|
4
4
|
* Byte-identical to `dopa_open_api::agent_http`, pinned by
|
|
5
|
-
* `libs/dopa-open
|
|
5
|
+
* `libs/dopa-open/api/vectors/agent_http_capability_v1.json`. A bearer token
|
|
6
6
|
* is a secret presented per request; a signature is not, so each capability is
|
|
7
7
|
* bound to the method, the request target, a digest of the exact body bytes,
|
|
8
8
|
* and a single-use nonce inside a bounded window. One minted to enter the
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* server reconstructs them from the request it actually received, so there is
|
|
14
14
|
* no restated copy to compare and therefore no comparison to forget.
|
|
15
15
|
*/
|
|
16
|
-
import { ByteWriter, frameSigningBytes, fromHex, textBytes, toHex } from "./bytes.js";
|
|
16
|
+
import { ByteReader, ByteWriter, frameSigningBytes, fromHex, textBytes, toHex, } from "./bytes.js";
|
|
17
17
|
import { blake2b256 } from "./crypto.js";
|
|
18
18
|
import { signRaw } from "./keypair.js";
|
|
19
19
|
const AGENT_HTTP_CAPABILITY_DOMAIN = textBytes("dopa_open::agent_http_capability::v1");
|
|
@@ -94,20 +94,17 @@ export function decodeAgentHttpHeader(value) {
|
|
|
94
94
|
const bytes = fromHex(value);
|
|
95
95
|
if (bytes.length !== HEADER_BYTES)
|
|
96
96
|
throw new Error(`capability header must be ${HEADER_BYTES} bytes, got ${bytes.length}`);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
agentPublicKey: bytes.slice(32, 64),
|
|
106
|
-
issuedAtMs: readU64(64),
|
|
107
|
-
expiresAtMs: readU64(72),
|
|
108
|
-
nonce: bytes.slice(80, 80 + AGENT_HTTP_CAPABILITY_NONCE_BYTES),
|
|
109
|
-
signature: bytes.slice(80 + AGENT_HTTP_CAPABILITY_NONCE_BYTES),
|
|
97
|
+
const reader = new ByteReader(bytes);
|
|
98
|
+
const capability = {
|
|
99
|
+
agentId: reader.readFixed(32, "agent id"),
|
|
100
|
+
agentPublicKey: reader.readFixed(32, "agent public key"),
|
|
101
|
+
issuedAtMs: reader.readU64("issued_at_ms"),
|
|
102
|
+
expiresAtMs: reader.readU64("expires_at_ms"),
|
|
103
|
+
nonce: reader.readFixed(AGENT_HTTP_CAPABILITY_NONCE_BYTES, "nonce"),
|
|
104
|
+
signature: reader.readFixed(SIGNATURE_BYTES, "signature"),
|
|
110
105
|
};
|
|
106
|
+
reader.finish();
|
|
107
|
+
return capability;
|
|
111
108
|
}
|
|
112
109
|
/** Mint and sign one capability, and return the header to send with it.
|
|
113
110
|
*
|
package/dist/bytes.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* These helpers exist so every framing in this package spells a number the
|
|
6
6
|
* same way; a preimage that drifts from the Rust client by one byte produces
|
|
7
7
|
* signatures the authority silently rejects, which is why the parity vectors
|
|
8
|
-
* in `libs/dopa-open
|
|
8
|
+
* in `libs/dopa-open/client-rs/vectors/` pin all of it.
|
|
9
9
|
*/
|
|
10
10
|
export function toHex(bytes) {
|
|
11
11
|
let out = "";
|
package/dist/claim.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { signRaw } from "./keypair.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ByteReader, ByteWriter, equalBytes, frameSigningBytes, fromHex, textBytes, toHex0x, } from "./bytes.js";
|
|
4
4
|
/* The agent's half of a claim: an invitation its own key signs.
|
|
5
5
|
*
|
|
6
6
|
* A wallet claims an agent by signing for it in the arena's UI. That proves
|
|
@@ -22,16 +22,6 @@ export const AGENT_CLAIM_INVITE_MAX_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
22
22
|
* nonce || signature`, every field fixed-width. */
|
|
23
23
|
export const AGENT_CLAIM_INVITE_TOKEN_BYTES = 32 + 32 + 32 + 8 + 8 + 16 + 64;
|
|
24
24
|
const ZERO_OWNER = new Uint8Array(32);
|
|
25
|
-
function u64be(value) {
|
|
26
|
-
const out = new Uint8Array(8);
|
|
27
|
-
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
function fixed(bytes, length, what) {
|
|
31
|
-
if (bytes.length !== length)
|
|
32
|
-
throw new Error(`${what} must be ${length} bytes, got ${bytes.length}`);
|
|
33
|
-
return bytes;
|
|
34
|
-
}
|
|
35
25
|
/** `AgentClaimInvite::canonical_payload` -- the framed fields. */
|
|
36
26
|
export function claimInviteCanonicalPayload(invite) {
|
|
37
27
|
if (invite.expiresAtMs <= invite.issuedAtMs)
|
|
@@ -41,7 +31,18 @@ export function claimInviteCanonicalPayload(invite) {
|
|
|
41
31
|
throw new Error("an invitation may stay open for at most a week");
|
|
42
32
|
if (invite.nonce.every((byte) => byte === 0))
|
|
43
33
|
throw new Error("the invitation's nonce must not be all zero");
|
|
44
|
-
return
|
|
34
|
+
return new ByteWriter()
|
|
35
|
+
.pushBytes(AGENT_CLAIM_INVITE_DOMAIN)
|
|
36
|
+
.pushByte(0)
|
|
37
|
+
.pushByte(CANONICAL_WIRE_VERSION)
|
|
38
|
+
.pushByte(INVITE_OPERATION)
|
|
39
|
+
.pushFixed(invite.agentId, 32, "agent id")
|
|
40
|
+
.pushFixed(invite.owner ?? ZERO_OWNER, 32, "owner")
|
|
41
|
+
.pushFixed(invite.agentPublicKey, 32, "agent public key")
|
|
42
|
+
.pushU64(invite.issuedAtMs)
|
|
43
|
+
.pushU64(invite.expiresAtMs)
|
|
44
|
+
.pushFixed(invite.nonce, AGENT_CLAIM_INVITE_NONCE_BYTES, "nonce")
|
|
45
|
+
.bytes();
|
|
45
46
|
}
|
|
46
47
|
/** The bytes the agent's key signs, raw ed25519. */
|
|
47
48
|
export function claimInviteSigningBytes(invite) {
|
|
@@ -66,7 +67,15 @@ export async function mintClaimInvite(agent, agentId, options = {}) {
|
|
|
66
67
|
function claimInviteBytes(invite) {
|
|
67
68
|
if (invite.signature.length !== 64)
|
|
68
69
|
throw new Error("an invitation is encoded only once it is signed");
|
|
69
|
-
return
|
|
70
|
+
return new ByteWriter()
|
|
71
|
+
.pushFixed(invite.agentId, 32, "agent id")
|
|
72
|
+
.pushFixed(invite.owner ?? ZERO_OWNER, 32, "owner")
|
|
73
|
+
.pushFixed(invite.agentPublicKey, 32, "agent public key")
|
|
74
|
+
.pushU64(invite.issuedAtMs)
|
|
75
|
+
.pushU64(invite.expiresAtMs)
|
|
76
|
+
.pushFixed(invite.nonce, AGENT_CLAIM_INVITE_NONCE_BYTES, "nonce")
|
|
77
|
+
.pushBytes(invite.signature)
|
|
78
|
+
.bytes();
|
|
70
79
|
}
|
|
71
80
|
/** The token the link carries and the claim body posts back: 0x-hex.
|
|
72
81
|
*
|
|
@@ -115,20 +124,27 @@ export function decodeClaimInvite(token) {
|
|
|
115
124
|
const bytes = claimInviteTokenBytes(token.trim());
|
|
116
125
|
if (bytes.length !== AGENT_CLAIM_INVITE_TOKEN_BYTES)
|
|
117
126
|
throw new Error(`an invitation token is ${AGENT_CLAIM_INVITE_TOKEN_BYTES} bytes, got ${bytes.length}`);
|
|
118
|
-
const
|
|
119
|
-
const
|
|
127
|
+
const reader = new ByteReader(bytes);
|
|
128
|
+
const agentId = reader.readFixed(32, "agent id");
|
|
129
|
+
const owner = reader.readFixed(32, "owner");
|
|
130
|
+
const agentPublicKey = reader.readFixed(32, "agent public key");
|
|
131
|
+
const issuedAtMs = reader.readU64("issued");
|
|
132
|
+
const expiresAtMs = reader.readU64("expires");
|
|
133
|
+
const nonce = reader.readFixed(AGENT_CLAIM_INVITE_NONCE_BYTES, "nonce");
|
|
134
|
+
const signature = reader.readFixed(64, "signature");
|
|
135
|
+
reader.finish();
|
|
120
136
|
return {
|
|
121
|
-
agentId
|
|
137
|
+
agentId,
|
|
122
138
|
owner: equalBytes(owner, ZERO_OWNER) ? null : owner,
|
|
123
|
-
agentPublicKey
|
|
124
|
-
issuedAtMs
|
|
125
|
-
expiresAtMs
|
|
126
|
-
nonce
|
|
127
|
-
signature
|
|
139
|
+
agentPublicKey,
|
|
140
|
+
issuedAtMs,
|
|
141
|
+
expiresAtMs,
|
|
142
|
+
nonce,
|
|
143
|
+
signature,
|
|
128
144
|
};
|
|
129
145
|
}
|
|
130
146
|
/** Where the owner goes to accept: the agent's claim page with the token. */
|
|
131
147
|
export function claimInviteLink(arenaOrigin, agentIdHex, token) {
|
|
132
148
|
const id = agentIdHex.startsWith("0x") ? agentIdHex : `0x${agentIdHex}`;
|
|
133
|
-
return `${arenaOrigin.replace(/\/$/, "")}/
|
|
149
|
+
return `${arenaOrigin.replace(/\/$/, "")}/open/agents/${id}/claim?invite=${token}`;
|
|
134
150
|
}
|
package/dist/cli.js
CHANGED
|
@@ -23,14 +23,20 @@ import { claimInviteLink, encodeClaimInvite, encodeClaimInviteCompact, mintClaim
|
|
|
23
23
|
import { acceptAndAwaitAdmission } from "./offer.js";
|
|
24
24
|
import { playTour, queueUntilSeated } from "./tour.js";
|
|
25
25
|
import { joinRoomWhenComposed, MIN_ROOM_SEATS, openRoom, roomInvitePrompt, } from "./room.js";
|
|
26
|
-
import { disputeHolding, JoinRefused, joinTransaction, leaveTransaction, listTournaments, matchmakingOverLine, planJoin, presentToTournament, readAgentEntry, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, } from "./openTournament.js";
|
|
27
|
-
import {
|
|
26
|
+
import { disputeHolding, JoinRefused, joinTransaction, leaveTransaction, listTournaments, matchmakingOverLine, planJoin, presentToTournament, readAgentEntry, readAgentEntrySettled, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, } from "./openTournament.js";
|
|
27
|
+
import { buildConsentRequest, DEFAULT_OPEN_API_BASE_URL, digestForPrompt, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
|
|
28
28
|
import { actionSigningBytes, joinSigningBytes, requireSessionVersion, resumeSigningBytes, SESSION_VERSION, } from "./sessionWire.js";
|
|
29
29
|
import { describeNext, refusalMessageFromText } from "./refusal.js";
|
|
30
30
|
function fail(message) {
|
|
31
31
|
console.error(`dopa-open: ${message}`);
|
|
32
32
|
process.exit(1);
|
|
33
33
|
}
|
|
34
|
+
/** Resolves `--product-url`, falling back to `DOPA_OPEN_PRODUCT_URL` and then
|
|
35
|
+
* to the deployed default — the same precedence and env name the Rust CLI
|
|
36
|
+
* uses, so a script that sets one variable configures both binaries. */
|
|
37
|
+
function resolveProductUrl(explicit) {
|
|
38
|
+
return explicit ?? process.env.DOPA_OPEN_PRODUCT_URL ?? DEFAULT_OPEN_API_BASE_URL;
|
|
39
|
+
}
|
|
34
40
|
const hex = (value, field) => {
|
|
35
41
|
if (typeof value !== "string")
|
|
36
42
|
fail(`${field} must be a hex string`);
|
|
@@ -116,9 +122,7 @@ async function commandRegister(args) {
|
|
|
116
122
|
"dry-run": { type: "boolean", default: false },
|
|
117
123
|
},
|
|
118
124
|
});
|
|
119
|
-
const productUrl = values["product-url"];
|
|
120
|
-
if (!productUrl && !values["dry-run"])
|
|
121
|
-
fail("--product-url is required (or pass --dry-run to print the request)");
|
|
125
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
122
126
|
const agent = loadKeypair(values.key);
|
|
123
127
|
const versions = (raw, flag) => {
|
|
124
128
|
const list = raw.split(",").map((piece) => Number.parseInt(piece, 10));
|
|
@@ -162,7 +166,7 @@ async function commandRegister(args) {
|
|
|
162
166
|
console.log(JSON.stringify(request, null, 2));
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
165
|
-
const response = await fetch(`${productUrl.replace(/\/$/, "")}/
|
|
169
|
+
const response = await fetch(`${productUrl.replace(/\/$/, "")}/v1/agents`, {
|
|
166
170
|
method: "POST",
|
|
167
171
|
headers: { "content-type": "application/json" },
|
|
168
172
|
body: JSON.stringify(request),
|
|
@@ -225,9 +229,7 @@ async function commandName(args) {
|
|
|
225
229
|
bio: { type: "string" },
|
|
226
230
|
},
|
|
227
231
|
});
|
|
228
|
-
const productUrl = values["product-url"];
|
|
229
|
-
if (!productUrl)
|
|
230
|
-
fail("--product-url is required");
|
|
232
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
231
233
|
if (!values["agent-id"])
|
|
232
234
|
fail("--agent-id is required");
|
|
233
235
|
if (!values.name && !values.handle && !values.bio)
|
|
@@ -258,7 +260,7 @@ async function commandName(args) {
|
|
|
258
260
|
* against what they believed, and the refusal for getting it wrong is a bare
|
|
259
261
|
* `stale_generation` that names neither. */
|
|
260
262
|
async function readKeyState(productUrl, agentId) {
|
|
261
|
-
const response = await fetch(`${productUrl}/
|
|
263
|
+
const response = await fetch(`${productUrl}/v1/agents/${encodeURIComponent(agentId)}`);
|
|
262
264
|
if (!response.ok)
|
|
263
265
|
fail(`could not read agent ${agentId} (${response.status})`);
|
|
264
266
|
const wire = (await response.json());
|
|
@@ -291,9 +293,7 @@ async function commandKey(args) {
|
|
|
291
293
|
"dry-run": { type: "boolean", default: false },
|
|
292
294
|
},
|
|
293
295
|
});
|
|
294
|
-
const productUrl = values["product-url"]
|
|
295
|
-
if (!productUrl)
|
|
296
|
-
fail("--product-url is required");
|
|
296
|
+
const productUrl = resolveProductUrl(values["product-url"]).replace(/\/$/, "");
|
|
297
297
|
const agentId = values["agent-id"];
|
|
298
298
|
if (!agentId)
|
|
299
299
|
fail("--agent-id is required");
|
|
@@ -356,7 +356,7 @@ async function commandKey(args) {
|
|
|
356
356
|
return;
|
|
357
357
|
}
|
|
358
358
|
const path = retire ? "revocations" : "key-rotations";
|
|
359
|
-
const response = await fetch(`${productUrl}/
|
|
359
|
+
const response = await fetch(`${productUrl}/v1/agents/${encodeURIComponent(agentId)}/${path}`, {
|
|
360
360
|
method: "POST",
|
|
361
361
|
headers: { "content-type": "application/json" },
|
|
362
362
|
body: JSON.stringify(request),
|
|
@@ -486,9 +486,7 @@ async function commandPlay(args) {
|
|
|
486
486
|
"disconnect-after-actions": { type: "string" },
|
|
487
487
|
},
|
|
488
488
|
});
|
|
489
|
-
const productUrl = values["product-url"];
|
|
490
|
-
if (!productUrl)
|
|
491
|
-
fail("--product-url is required");
|
|
489
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
492
490
|
if (!values["agent-id"])
|
|
493
491
|
fail("--agent-id is required");
|
|
494
492
|
/* A tour seat is played over the Product API, so it needs none of the
|
|
@@ -581,7 +579,7 @@ function runRecordPath(keyFile) {
|
|
|
581
579
|
}
|
|
582
580
|
function writeRunRecord(keyFile, record) {
|
|
583
581
|
const path = runRecordPath(keyFile);
|
|
584
|
-
const watchUrl = `${record.productUrl.replace(/\/$/, "")}/
|
|
582
|
+
const watchUrl = `${record.productUrl.replace(/\/$/, "")}/open/matches/0x${record.executionId.replace(/^0x/i, "")}`;
|
|
585
583
|
writeFileSync(path, `${JSON.stringify({ ...record, watchUrl, at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
586
584
|
return path;
|
|
587
585
|
}
|
|
@@ -642,7 +640,7 @@ async function signedGet(productUrl, target, agent, agentIdHex) {
|
|
|
642
640
|
headers: { [AGENT_HTTP_CAPABILITY_HEADER]: header },
|
|
643
641
|
});
|
|
644
642
|
}
|
|
645
|
-
/** `dopa-open me`:
|
|
643
|
+
/** `dopa-open me`: Open's record of this agent, signed for.
|
|
646
644
|
*
|
|
647
645
|
* Both agents that played on 2026-09-06 wrote this request by hand, against
|
|
648
646
|
* the skill's own warning that a hand-written signature drifts by a byte. */
|
|
@@ -657,13 +655,13 @@ async function signedGet(productUrl, target, agent, agentIdHex) {
|
|
|
657
655
|
async function resolveAgentId(productUrl, ownerAddressHex, given) {
|
|
658
656
|
if (given)
|
|
659
657
|
return given;
|
|
660
|
-
const response = await fetch(`${productUrl.replace(/\/$/, "")}/
|
|
658
|
+
const response = await fetch(`${productUrl.replace(/\/$/, "")}/v1/agents?owner=${ownerAddressHex}&limit=60`);
|
|
661
659
|
if (!response.ok)
|
|
662
660
|
fail(`--agent-id was not given and the roster could not be read (${response.status})`);
|
|
663
661
|
const body = (await response.json());
|
|
664
662
|
const agents = body.agents ?? [];
|
|
665
663
|
if (agents.length === 0)
|
|
666
|
-
fail(`no agent on this
|
|
664
|
+
fail(`no agent on this Open deployment is claimed by ${ownerAddressHex}. The roster lists an agent under the wallet that ` +
|
|
667
665
|
"claimed it, so one that registered itself is not here until it is claimed; pass --agent-id (register printed it)");
|
|
668
666
|
if (agents.length > 1)
|
|
669
667
|
fail(`this key owns ${agents.length} agents; pass --agent-id to say which:\n ${agents
|
|
@@ -680,16 +678,14 @@ async function commandMe(args) {
|
|
|
680
678
|
"agent-id": { type: "string" },
|
|
681
679
|
},
|
|
682
680
|
});
|
|
683
|
-
const productUrl = values["product-url"];
|
|
684
|
-
if (!productUrl)
|
|
685
|
-
fail("--product-url is required");
|
|
681
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
686
682
|
const agent = loadKeypair(values.key);
|
|
687
683
|
/* Without `--agent-id`, ask the roster which agent this key owns. */
|
|
688
684
|
const agentId = await resolveAgentId(productUrl, agent.ownerAddressHex, values["agent-id"]);
|
|
689
|
-
const response = await signedGet(productUrl, "/
|
|
685
|
+
const response = await signedGet(productUrl, "/v1/agent/me", agent, agentId);
|
|
690
686
|
const body = await response.text();
|
|
691
687
|
if (!response.ok)
|
|
692
|
-
fail(refusalMessageFromText(`agent/me at ${productUrl}/
|
|
688
|
+
fail(refusalMessageFromText(`agent/me at ${productUrl}/v1/agent/me`, response.status, body));
|
|
693
689
|
try {
|
|
694
690
|
console.log(JSON.stringify(JSON.parse(body), null, 2));
|
|
695
691
|
}
|
|
@@ -760,6 +756,11 @@ async function commandTurn(args) {
|
|
|
760
756
|
agent,
|
|
761
757
|
agentId: fromHex(agentId),
|
|
762
758
|
waitMs: values.wait === undefined ? 0 : Number(values.wait) * 1000,
|
|
759
|
+
/* Written before each acknowledgement, not once at the end. `turn`
|
|
760
|
+
acknowledges the viewless join and the named prepared/open notices as
|
|
761
|
+
it takes them, and an acknowledgement the file does not record is a
|
|
762
|
+
phase the next process cannot know it is in. */
|
|
763
|
+
checkpoint: (next) => saveSeatState(statePath, next),
|
|
763
764
|
});
|
|
764
765
|
saveSeatState(statePath, outcome.state);
|
|
765
766
|
if (outcome.kind === "your-turn") {
|
|
@@ -852,14 +853,19 @@ async function commandAct(args) {
|
|
|
852
853
|
* this, so it is worth a caller of its own rather than a flag on the play
|
|
853
854
|
* loop. Prints what the authority recorded. */
|
|
854
855
|
async function consentToTerminal(options) {
|
|
855
|
-
const offerResponse = await fetch(`${options.productUrl.replace(/\/$/, "")}/
|
|
856
|
+
const offerResponse = await fetch(`${options.productUrl.replace(/\/$/, "")}/v1/playground/matches/${options.offerId}`);
|
|
856
857
|
const offerBody = await offerResponse.text();
|
|
857
858
|
if (!offerResponse.ok)
|
|
858
859
|
fail(`offer read failed (${offerResponse.status}): ${offerBody}`);
|
|
859
860
|
const record = JSON.parse(offerBody);
|
|
860
861
|
if (!record.admission)
|
|
861
862
|
fail("offer is not admitted; there is no execution to settle");
|
|
862
|
-
|
|
863
|
+
/* The settlement verbs live on the product origin — `/v1/authority/…` is
|
|
864
|
+
the product's public route, which forwards to the shard that admitted
|
|
865
|
+
the execution. The session's own origin serves `/v1/exec/` only, so
|
|
866
|
+
deriving the consent root from `session_base_url` asks a listener that
|
|
867
|
+
does not serve this spelling. */
|
|
868
|
+
const origin = options.productUrl.replace(/\/$/, "");
|
|
863
869
|
const path = settlementConsentPath(record.admission.execution_id);
|
|
864
870
|
const promptResponse = await fetch(`${origin}${path}`);
|
|
865
871
|
const promptText = await promptResponse.text();
|
|
@@ -1002,9 +1008,7 @@ async function commandConsent(args) {
|
|
|
1002
1008
|
"agent-id": { type: "string" },
|
|
1003
1009
|
},
|
|
1004
1010
|
});
|
|
1005
|
-
const productUrl = values["product-url"];
|
|
1006
|
-
if (!productUrl)
|
|
1007
|
-
fail("--product-url is required");
|
|
1011
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1008
1012
|
if (!values.offer)
|
|
1009
1013
|
fail("--offer is required");
|
|
1010
1014
|
if (!values.seat)
|
|
@@ -1040,9 +1044,7 @@ async function commandQueue(args) {
|
|
|
1040
1044
|
play: { type: "boolean", default: false },
|
|
1041
1045
|
},
|
|
1042
1046
|
});
|
|
1043
|
-
const productUrl = values["product-url"];
|
|
1044
|
-
if (!productUrl)
|
|
1045
|
-
fail("--product-url is required");
|
|
1047
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1046
1048
|
if (!values["agent-id"])
|
|
1047
1049
|
fail("--agent-id is required");
|
|
1048
1050
|
const tour = values.tour;
|
|
@@ -1086,7 +1088,7 @@ async function commandQueue(args) {
|
|
|
1086
1088
|
Labelled `agent_page`, never `watch`. It was `watch` for one release, and
|
|
1087
1089
|
an agent reading that line handed its operator a profile under the word
|
|
1088
1090
|
WATCH; `watch` is the table, printed below once there is one. */
|
|
1089
|
-
console.log(`agent_page ${client.productUrl.replace(/\/$/, "")}/
|
|
1091
|
+
console.log(`agent_page ${client.productUrl.replace(/\/$/, "")}/open/agents/0x${values["agent-id"].replace(/^0x/i, "")}`);
|
|
1090
1092
|
let lastWaitingLine = "";
|
|
1091
1093
|
const seated = await queueUntilSeated(client, tour, {
|
|
1092
1094
|
minAgents,
|
|
@@ -1141,7 +1143,7 @@ async function commandQueue(args) {
|
|
|
1141
1143
|
executionId: admitted.executionId,
|
|
1142
1144
|
};
|
|
1143
1145
|
const recordPath = writeRunRecord(values.key, runRecord);
|
|
1144
|
-
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/
|
|
1146
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/open/matches/0x${admitted.executionId.replace(/^0x/i, "")}`);
|
|
1145
1147
|
console.log(`run_record ${recordPath}`);
|
|
1146
1148
|
console.log(`reconnect ${reconnectCommand(values.key, runRecord)}`);
|
|
1147
1149
|
takeSeatLock(values.key, values["agent-id"]);
|
|
@@ -1233,9 +1235,7 @@ async function commandRoom(args) {
|
|
|
1233
1235
|
"timeout-ms": { type: "string", default: String(30 * 60_000) },
|
|
1234
1236
|
},
|
|
1235
1237
|
});
|
|
1236
|
-
const productUrl = values["product-url"];
|
|
1237
|
-
if (!productUrl)
|
|
1238
|
-
fail("--product-url is required");
|
|
1238
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1239
1239
|
if (!values["agent-id"])
|
|
1240
1240
|
fail("--agent-id is required");
|
|
1241
1241
|
const client = {
|
|
@@ -1255,6 +1255,12 @@ async function commandRoom(args) {
|
|
|
1255
1255
|
const room = await openRoom(client, seats);
|
|
1256
1256
|
console.log(`table ${room.tableId}`);
|
|
1257
1257
|
console.log(`seats ${room.seatCount}`);
|
|
1258
|
+
/* How full it is and what fills it. A room seats nobody but the agents
|
|
1259
|
+
invited to it: the chairs nobody takes stay empty, so an opener has to
|
|
1260
|
+
know how many are still open to know who to send the invite to. */
|
|
1261
|
+
console.log(`seated 1 of ${room.seatCount}`);
|
|
1262
|
+
console.log("fill the room deals when its last seat is taken, or two minutes after " +
|
|
1263
|
+
"its second agent sits, whichever comes first; untaken seats stay empty");
|
|
1258
1264
|
console.log(`mode ${room.mode}`);
|
|
1259
1265
|
console.log(`settlement ${room.settlement}`);
|
|
1260
1266
|
if (room.executionId)
|
|
@@ -1263,12 +1269,12 @@ async function commandRoom(args) {
|
|
|
1263
1269
|
operator forwards the whole thing rather than the table id alone: an id
|
|
1264
1270
|
without the document is how a guest ends up asking the table routes for
|
|
1265
1271
|
a decision. */
|
|
1266
|
-
console.log(`invite ${roomInvitePrompt(client.productUrl, room.tableId)}`);
|
|
1272
|
+
console.log(`invite ${roomInvitePrompt(client.productUrl, room.tableId, room.seatCount - 1)}`);
|
|
1267
1273
|
/* The room's own page, which exists before any hand does. Not `watch`:
|
|
1268
1274
|
that word is the match link on every door, and `room join` prints it
|
|
1269
1275
|
once the room composes, so two different links under one label was a
|
|
1270
1276
|
thing an agent had to be warned about. */
|
|
1271
|
-
console.log(`room_page ${client.productUrl.replace(/\/$/, "")}/
|
|
1277
|
+
console.log(`room_page ${client.productUrl.replace(/\/$/, "")}/open/tours/private-room/tables/${room.tableId}`);
|
|
1272
1278
|
return;
|
|
1273
1279
|
}
|
|
1274
1280
|
const tableId = values["table-id"];
|
|
@@ -1276,10 +1282,23 @@ async function commandRoom(args) {
|
|
|
1276
1282
|
fail("--table-id is required: the id the room's opener sent you");
|
|
1277
1283
|
const joined = await joinRoomWhenComposed(client, tableId, {
|
|
1278
1284
|
timeoutMs: Number(values["timeout-ms"]),
|
|
1279
|
-
onWaiting: () =>
|
|
1285
|
+
onWaiting: (found) => {
|
|
1286
|
+
/* What the room is waiting for, said in its own numbers where the join
|
|
1287
|
+
answered with them. An opener's own join is refused rather than
|
|
1288
|
+
answered, so it has none to print. */
|
|
1289
|
+
const seats = found?.seated === undefined || found.seatCount === undefined
|
|
1290
|
+
? ""
|
|
1291
|
+
: ` ${found.seated} of ${found.seatCount} seated;`;
|
|
1292
|
+
const deals = found?.fillAtMs === undefined
|
|
1293
|
+
? " it deals when a second agent sits"
|
|
1294
|
+
: ` it deals by ${new Date(found.fillAtMs).toISOString()} or sooner if its last seat is taken`;
|
|
1295
|
+
console.log(`waiting_for_room table ${tableId} holds this agent's seat;${seats}${deals}`);
|
|
1296
|
+
},
|
|
1280
1297
|
});
|
|
1281
1298
|
console.log(`table ${joined.tableId}`);
|
|
1282
1299
|
console.log(`joined ${joined.joined}`);
|
|
1300
|
+
if (joined.seated !== undefined && joined.seatCount !== undefined)
|
|
1301
|
+
console.log(`seated ${joined.seated} of ${joined.seatCount}`);
|
|
1283
1302
|
if (joined.offerId === undefined) {
|
|
1284
1303
|
/* A mock stack composes no offer, so there is no seat to accept and
|
|
1285
1304
|
nothing for `--play` to drive. Said plainly rather than by an empty
|
|
@@ -1304,7 +1323,7 @@ async function commandRoom(args) {
|
|
|
1304
1323
|
executionId: admitted.executionId,
|
|
1305
1324
|
};
|
|
1306
1325
|
const recordPath = writeRunRecord(values.key, runRecord);
|
|
1307
|
-
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/
|
|
1326
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/open/matches/0x${admitted.executionId.replace(/^0x/i, "")}`);
|
|
1308
1327
|
console.log(`run_record ${recordPath}`);
|
|
1309
1328
|
console.log(`reconnect ${reconnectCommand(values.key, runRecord)}`);
|
|
1310
1329
|
takeSeatLock(values.key, values["agent-id"]);
|
|
@@ -1371,9 +1390,7 @@ async function commandTournament(args) {
|
|
|
1371
1390
|
"until-out": { type: "boolean", default: false },
|
|
1372
1391
|
},
|
|
1373
1392
|
});
|
|
1374
|
-
const productUrl = values["product-url"];
|
|
1375
|
-
if (!productUrl)
|
|
1376
|
-
fail("--product-url is required");
|
|
1393
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1377
1394
|
const agent = loadKeypair(values.key);
|
|
1378
1395
|
const tournamentId = await tournamentArg(productUrl, values.tournament);
|
|
1379
1396
|
const overview = await readTournament(productUrl, tournamentId);
|
|
@@ -1384,7 +1401,7 @@ async function commandTournament(args) {
|
|
|
1384
1401
|
this agent holds now is on the owner's side of the book. */
|
|
1385
1402
|
const playTakenBackNext = () => describeNext({
|
|
1386
1403
|
action: "read",
|
|
1387
|
-
route: `/
|
|
1404
|
+
route: `/v1/tournaments/${tournamentId}/owners/${owner ?? "{owner}"}`,
|
|
1388
1405
|
});
|
|
1389
1406
|
const held = async () => owner ? await playsHeldBy(productUrl, tournamentId, owner, chip) : [];
|
|
1390
1407
|
let plays = await held();
|
|
@@ -1447,8 +1464,16 @@ async function commandTournament(args) {
|
|
|
1447
1464
|
return;
|
|
1448
1465
|
}
|
|
1449
1466
|
if (verb === "leave") {
|
|
1450
|
-
|
|
1451
|
-
|
|
1467
|
+
/* An agent that queued a moment ago is on chain and not yet in the book,
|
|
1468
|
+
which is read a checkpoint behind it. Refusing on the first read told an
|
|
1469
|
+
agent that had just been printed `queued waiting` that it was not in the
|
|
1470
|
+
book at all, and the refusal's own `next` sent it to join again — which
|
|
1471
|
+
would queue an agent that is already queued. So an absent entry is
|
|
1472
|
+
waited out, briefly, before it is believed. */
|
|
1473
|
+
const settled = entry ??
|
|
1474
|
+
(await readAgentEntrySettled(productUrl, tournamentId, chip));
|
|
1475
|
+
if (settled?.state !== "queued")
|
|
1476
|
+
fail(`this agent is ${settled?.state ?? "not in the book"}; only a queued agent can leave, and a seated one plays its table out`);
|
|
1452
1477
|
const executed = await sponsorAndExecute(client, leaveTransaction(overview, chip));
|
|
1453
1478
|
console.log(`left ${executed.digest} ${executed.status}`);
|
|
1454
1479
|
if (executed.status !== "success")
|
|
@@ -1610,7 +1635,7 @@ async function commandTournament(args) {
|
|
|
1610
1635
|
console.log(`offer ${offerId}`);
|
|
1611
1636
|
console.log(`seat ${seated.seat}`);
|
|
1612
1637
|
if (seated.executionId)
|
|
1613
|
-
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/
|
|
1638
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/open/matches/${seated.executionId}`);
|
|
1614
1639
|
if (!values.play)
|
|
1615
1640
|
return;
|
|
1616
1641
|
const seat = seated.seat;
|
|
@@ -1652,7 +1677,7 @@ async function commandTournament(args) {
|
|
|
1652
1677
|
}
|
|
1653
1678
|
/** `dopa-open claim-invite`: the link a wallet needs to claim this agent.
|
|
1654
1679
|
*
|
|
1655
|
-
* A claim is two consents. The wallet signs on
|
|
1680
|
+
* A claim is two consents. The wallet signs on Open's claim page; you, holding
|
|
1656
1681
|
* the agent's key, sign the invitation that lets it. Name the wallet with
|
|
1657
1682
|
* `--owner` to make the link good for that wallet alone; leave it out and the
|
|
1658
1683
|
* link is good for whoever opens it, for as long as `--hours` says. */
|
|
@@ -1668,9 +1693,7 @@ async function commandClaimInvite(args) {
|
|
|
1668
1693
|
hours: { type: "string", default: "24" },
|
|
1669
1694
|
},
|
|
1670
1695
|
});
|
|
1671
|
-
const productUrl = values["product-url"]
|
|
1672
|
-
if (!productUrl)
|
|
1673
|
-
fail("--product-url is required");
|
|
1696
|
+
const productUrl = resolveProductUrl(values["product-url"]).replace(/\/$/, "");
|
|
1674
1697
|
if (!values["agent-id"])
|
|
1675
1698
|
fail("--agent-id is required");
|
|
1676
1699
|
const hours = Number(values.hours);
|
|
@@ -1688,7 +1711,7 @@ async function commandClaimInvite(args) {
|
|
|
1688
1711
|
expects the old form. Both decode to the same 192 bytes. */
|
|
1689
1712
|
const token = encodeClaimInvite(invite);
|
|
1690
1713
|
const linkToken = encodeClaimInviteCompact(invite);
|
|
1691
|
-
/*
|
|
1714
|
+
/* Open's pages and its API share an origin on a deployment; a local
|
|
1692
1715
|
stack serves them apart, which is what --arena-url is for. */
|
|
1693
1716
|
const arena = (values["arena-url"] ?? productUrl).replace(/\/$/, "");
|
|
1694
1717
|
console.log(`invite ${token}`);
|
|
@@ -1700,7 +1723,7 @@ const USAGE = `usage: dopa-open <command>
|
|
|
1700
1723
|
|
|
1701
1724
|
keygen generate a keypair into .dopa-keypair (Sui suiprivkey format)
|
|
1702
1725
|
address print the owner address and public key of an existing key file
|
|
1703
|
-
me print
|
|
1726
|
+
me print Open's record of this agent, signed for (agent/me)
|
|
1704
1727
|
claim-invite
|
|
1705
1728
|
mint the claim link a wallet needs to claim this agent
|
|
1706
1729
|
register self-allocate and register the agent with a product deployment
|
package/dist/identity.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* and this key can no longer sign for it -- which is the point of a claim.
|
|
8
8
|
*
|
|
9
9
|
* The payload mirrors `AgentIdentityEdit::canonical_payload` in
|
|
10
|
-
* `backend/dopa-open/product/src/domain/custodial/key_rotation.rs`:
|
|
11
|
-
* field is length-framed, so "ab"/"c"
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* `backend/dopa-open/product/src/domain/custodial/key_rotation.rs`: the two
|
|
11
|
+
* ids are fixed 32 bytes and every named field is length-framed, so "ab"/"c"
|
|
12
|
+
* and "a"/"bc" cannot sign the same bytes, and neither can a shorter agent id
|
|
13
|
+
* borrow a byte from the owner behind it. The product parses before it
|
|
14
|
+
* verifies -- it trims each field and lower-cases the handle -- so this signs
|
|
15
|
+
* the parsed form, or the signature is over bytes the store never holds. */
|
|
16
|
+
import { ByteWriter, fromHex, textBytes } from "./bytes.js";
|
|
15
17
|
import { signOwnerAuthenticator } from "./keypair.js";
|
|
16
18
|
import { refusalMessageFromText } from "./refusal.js";
|
|
17
19
|
const IDENTITY_DOMAIN = "dopa_open::agent_identity::v1";
|
|
@@ -25,40 +27,20 @@ export function parseIdentityFields(fields) {
|
|
|
25
27
|
bio: fields.bio?.trim() || null,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
|
-
function u64be(value) {
|
|
29
|
-
const out = new Uint8Array(8);
|
|
30
|
-
new DataView(out.buffer).setBigUint64(0, BigInt(value));
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
function fromHex(value) {
|
|
34
|
-
const hex = value.replace(/^0x/i, "");
|
|
35
|
-
const out = new Uint8Array(hex.length / 2);
|
|
36
|
-
for (let index = 0; index < out.length; index++)
|
|
37
|
-
out[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
30
|
/** The exact bytes the owning address signs to name an agent. */
|
|
41
31
|
export function canonicalIdentityPayload(edit) {
|
|
42
|
-
const
|
|
43
|
-
const domain = encoder.encode(IDENTITY_DOMAIN);
|
|
32
|
+
const domain = textBytes(IDENTITY_DOMAIN);
|
|
44
33
|
const parsed = parseIdentityFields(edit.fields);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
fromHex(edit.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
u64be(edit.expiresAtMs),
|
|
54
|
-
];
|
|
55
|
-
const out = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
56
|
-
let offset = 0;
|
|
57
|
-
for (const chunk of chunks) {
|
|
58
|
-
out.set(chunk, offset);
|
|
59
|
-
offset += chunk.length;
|
|
34
|
+
const writer = new ByteWriter()
|
|
35
|
+
.pushU64(domain.length)
|
|
36
|
+
.pushBytes(domain)
|
|
37
|
+
.pushFixed(fromHex(edit.agentId), 32, "agent id")
|
|
38
|
+
.pushFixed(fromHex(edit.owner), 32, "owner");
|
|
39
|
+
for (const field of [parsed.name ?? "", parsed.handle ?? "", parsed.bio ?? ""]) {
|
|
40
|
+
const bytes = textBytes(field);
|
|
41
|
+
writer.pushU64(bytes.length).pushBytes(bytes);
|
|
60
42
|
}
|
|
61
|
-
return
|
|
43
|
+
return writer.pushU64(edit.issuedAtMs).pushU64(edit.expiresAtMs).bytes();
|
|
62
44
|
}
|
|
63
45
|
/** Name an agent, as the address that owns it. Answers what the arena stored. */
|
|
64
46
|
export async function nameAgent(args) {
|
|
@@ -73,8 +55,8 @@ export async function nameAgent(args) {
|
|
|
73
55
|
issuedAtMs,
|
|
74
56
|
expiresAtMs,
|
|
75
57
|
}));
|
|
76
|
-
const agentId = `0x${args.agentId.replace(/^0x
|
|
77
|
-
const response = await fetchImpl(`${args.productUrl.replace(/\/$/, "")}/
|
|
58
|
+
const agentId = `0x${args.agentId.replace(/^0x/, "")}`;
|
|
59
|
+
const response = await fetchImpl(`${args.productUrl.replace(/\/$/, "")}/v1/agents/${agentId}/identity`, {
|
|
78
60
|
method: "PUT",
|
|
79
61
|
headers: { "content-type": "application/json" },
|
|
80
62
|
body: JSON.stringify({
|