@dopamint-fun/open-sdk 0.2.0-dev.1 → 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 +53 -54
- package/dist/identity.js +19 -37
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- 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.js +2 -2
- package/dist/seatState.d.ts +1 -0
- package/dist/seatState.js +6 -0
- package/dist/seatTurn.js +1 -1
- package/dist/session.d.ts +12 -0
- package/dist/session.js +63 -9
- package/dist/sessionCodec.d.ts +4 -0
- package/dist/sessionCodec.js +18 -3
- package/dist/sessionWire.js +1 -1
- 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
|
}
|
|
@@ -857,14 +853,19 @@ async function commandAct(args) {
|
|
|
857
853
|
* this, so it is worth a caller of its own rather than a flag on the play
|
|
858
854
|
* loop. Prints what the authority recorded. */
|
|
859
855
|
async function consentToTerminal(options) {
|
|
860
|
-
const offerResponse = await fetch(`${options.productUrl.replace(/\/$/, "")}/
|
|
856
|
+
const offerResponse = await fetch(`${options.productUrl.replace(/\/$/, "")}/v1/playground/matches/${options.offerId}`);
|
|
861
857
|
const offerBody = await offerResponse.text();
|
|
862
858
|
if (!offerResponse.ok)
|
|
863
859
|
fail(`offer read failed (${offerResponse.status}): ${offerBody}`);
|
|
864
860
|
const record = JSON.parse(offerBody);
|
|
865
861
|
if (!record.admission)
|
|
866
862
|
fail("offer is not admitted; there is no execution to settle");
|
|
867
|
-
|
|
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(/\/$/, "");
|
|
868
869
|
const path = settlementConsentPath(record.admission.execution_id);
|
|
869
870
|
const promptResponse = await fetch(`${origin}${path}`);
|
|
870
871
|
const promptText = await promptResponse.text();
|
|
@@ -1007,9 +1008,7 @@ async function commandConsent(args) {
|
|
|
1007
1008
|
"agent-id": { type: "string" },
|
|
1008
1009
|
},
|
|
1009
1010
|
});
|
|
1010
|
-
const productUrl = values["product-url"];
|
|
1011
|
-
if (!productUrl)
|
|
1012
|
-
fail("--product-url is required");
|
|
1011
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1013
1012
|
if (!values.offer)
|
|
1014
1013
|
fail("--offer is required");
|
|
1015
1014
|
if (!values.seat)
|
|
@@ -1045,9 +1044,7 @@ async function commandQueue(args) {
|
|
|
1045
1044
|
play: { type: "boolean", default: false },
|
|
1046
1045
|
},
|
|
1047
1046
|
});
|
|
1048
|
-
const productUrl = values["product-url"];
|
|
1049
|
-
if (!productUrl)
|
|
1050
|
-
fail("--product-url is required");
|
|
1047
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1051
1048
|
if (!values["agent-id"])
|
|
1052
1049
|
fail("--agent-id is required");
|
|
1053
1050
|
const tour = values.tour;
|
|
@@ -1091,7 +1088,7 @@ async function commandQueue(args) {
|
|
|
1091
1088
|
Labelled `agent_page`, never `watch`. It was `watch` for one release, and
|
|
1092
1089
|
an agent reading that line handed its operator a profile under the word
|
|
1093
1090
|
WATCH; `watch` is the table, printed below once there is one. */
|
|
1094
|
-
console.log(`agent_page ${client.productUrl.replace(/\/$/, "")}/
|
|
1091
|
+
console.log(`agent_page ${client.productUrl.replace(/\/$/, "")}/open/agents/0x${values["agent-id"].replace(/^0x/i, "")}`);
|
|
1095
1092
|
let lastWaitingLine = "";
|
|
1096
1093
|
const seated = await queueUntilSeated(client, tour, {
|
|
1097
1094
|
minAgents,
|
|
@@ -1146,7 +1143,7 @@ async function commandQueue(args) {
|
|
|
1146
1143
|
executionId: admitted.executionId,
|
|
1147
1144
|
};
|
|
1148
1145
|
const recordPath = writeRunRecord(values.key, runRecord);
|
|
1149
|
-
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/
|
|
1146
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/open/matches/0x${admitted.executionId.replace(/^0x/i, "")}`);
|
|
1150
1147
|
console.log(`run_record ${recordPath}`);
|
|
1151
1148
|
console.log(`reconnect ${reconnectCommand(values.key, runRecord)}`);
|
|
1152
1149
|
takeSeatLock(values.key, values["agent-id"]);
|
|
@@ -1238,9 +1235,7 @@ async function commandRoom(args) {
|
|
|
1238
1235
|
"timeout-ms": { type: "string", default: String(30 * 60_000) },
|
|
1239
1236
|
},
|
|
1240
1237
|
});
|
|
1241
|
-
const productUrl = values["product-url"];
|
|
1242
|
-
if (!productUrl)
|
|
1243
|
-
fail("--product-url is required");
|
|
1238
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1244
1239
|
if (!values["agent-id"])
|
|
1245
1240
|
fail("--agent-id is required");
|
|
1246
1241
|
const client = {
|
|
@@ -1279,7 +1274,7 @@ async function commandRoom(args) {
|
|
|
1279
1274
|
that word is the match link on every door, and `room join` prints it
|
|
1280
1275
|
once the room composes, so two different links under one label was a
|
|
1281
1276
|
thing an agent had to be warned about. */
|
|
1282
|
-
console.log(`room_page ${client.productUrl.replace(/\/$/, "")}/
|
|
1277
|
+
console.log(`room_page ${client.productUrl.replace(/\/$/, "")}/open/tours/private-room/tables/${room.tableId}`);
|
|
1283
1278
|
return;
|
|
1284
1279
|
}
|
|
1285
1280
|
const tableId = values["table-id"];
|
|
@@ -1328,7 +1323,7 @@ async function commandRoom(args) {
|
|
|
1328
1323
|
executionId: admitted.executionId,
|
|
1329
1324
|
};
|
|
1330
1325
|
const recordPath = writeRunRecord(values.key, runRecord);
|
|
1331
|
-
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/
|
|
1326
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/open/matches/0x${admitted.executionId.replace(/^0x/i, "")}`);
|
|
1332
1327
|
console.log(`run_record ${recordPath}`);
|
|
1333
1328
|
console.log(`reconnect ${reconnectCommand(values.key, runRecord)}`);
|
|
1334
1329
|
takeSeatLock(values.key, values["agent-id"]);
|
|
@@ -1395,9 +1390,7 @@ async function commandTournament(args) {
|
|
|
1395
1390
|
"until-out": { type: "boolean", default: false },
|
|
1396
1391
|
},
|
|
1397
1392
|
});
|
|
1398
|
-
const productUrl = values["product-url"];
|
|
1399
|
-
if (!productUrl)
|
|
1400
|
-
fail("--product-url is required");
|
|
1393
|
+
const productUrl = resolveProductUrl(values["product-url"]);
|
|
1401
1394
|
const agent = loadKeypair(values.key);
|
|
1402
1395
|
const tournamentId = await tournamentArg(productUrl, values.tournament);
|
|
1403
1396
|
const overview = await readTournament(productUrl, tournamentId);
|
|
@@ -1408,7 +1401,7 @@ async function commandTournament(args) {
|
|
|
1408
1401
|
this agent holds now is on the owner's side of the book. */
|
|
1409
1402
|
const playTakenBackNext = () => describeNext({
|
|
1410
1403
|
action: "read",
|
|
1411
|
-
route: `/
|
|
1404
|
+
route: `/v1/tournaments/${tournamentId}/owners/${owner ?? "{owner}"}`,
|
|
1412
1405
|
});
|
|
1413
1406
|
const held = async () => owner ? await playsHeldBy(productUrl, tournamentId, owner, chip) : [];
|
|
1414
1407
|
let plays = await held();
|
|
@@ -1471,8 +1464,16 @@ async function commandTournament(args) {
|
|
|
1471
1464
|
return;
|
|
1472
1465
|
}
|
|
1473
1466
|
if (verb === "leave") {
|
|
1474
|
-
|
|
1475
|
-
|
|
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`);
|
|
1476
1477
|
const executed = await sponsorAndExecute(client, leaveTransaction(overview, chip));
|
|
1477
1478
|
console.log(`left ${executed.digest} ${executed.status}`);
|
|
1478
1479
|
if (executed.status !== "success")
|
|
@@ -1634,7 +1635,7 @@ async function commandTournament(args) {
|
|
|
1634
1635
|
console.log(`offer ${offerId}`);
|
|
1635
1636
|
console.log(`seat ${seated.seat}`);
|
|
1636
1637
|
if (seated.executionId)
|
|
1637
|
-
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/
|
|
1638
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/open/matches/${seated.executionId}`);
|
|
1638
1639
|
if (!values.play)
|
|
1639
1640
|
return;
|
|
1640
1641
|
const seat = seated.seat;
|
|
@@ -1676,7 +1677,7 @@ async function commandTournament(args) {
|
|
|
1676
1677
|
}
|
|
1677
1678
|
/** `dopa-open claim-invite`: the link a wallet needs to claim this agent.
|
|
1678
1679
|
*
|
|
1679
|
-
* 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
|
|
1680
1681
|
* the agent's key, sign the invitation that lets it. Name the wallet with
|
|
1681
1682
|
* `--owner` to make the link good for that wallet alone; leave it out and the
|
|
1682
1683
|
* link is good for whoever opens it, for as long as `--hours` says. */
|
|
@@ -1692,9 +1693,7 @@ async function commandClaimInvite(args) {
|
|
|
1692
1693
|
hours: { type: "string", default: "24" },
|
|
1693
1694
|
},
|
|
1694
1695
|
});
|
|
1695
|
-
const productUrl = values["product-url"]
|
|
1696
|
-
if (!productUrl)
|
|
1697
|
-
fail("--product-url is required");
|
|
1696
|
+
const productUrl = resolveProductUrl(values["product-url"]).replace(/\/$/, "");
|
|
1698
1697
|
if (!values["agent-id"])
|
|
1699
1698
|
fail("--agent-id is required");
|
|
1700
1699
|
const hours = Number(values.hours);
|
|
@@ -1712,7 +1711,7 @@ async function commandClaimInvite(args) {
|
|
|
1712
1711
|
expects the old form. Both decode to the same 192 bytes. */
|
|
1713
1712
|
const token = encodeClaimInvite(invite);
|
|
1714
1713
|
const linkToken = encodeClaimInviteCompact(invite);
|
|
1715
|
-
/*
|
|
1714
|
+
/* Open's pages and its API share an origin on a deployment; a local
|
|
1716
1715
|
stack serves them apart, which is what --arena-url is for. */
|
|
1717
1716
|
const arena = (values["arena-url"] ?? productUrl).replace(/\/$/, "");
|
|
1718
1717
|
console.log(`invite ${token}`);
|
|
@@ -1724,7 +1723,7 @@ const USAGE = `usage: dopa-open <command>
|
|
|
1724
1723
|
|
|
1725
1724
|
keygen generate a keypair into .dopa-keypair (Sui suiprivkey format)
|
|
1726
1725
|
address print the owner address and public key of an existing key file
|
|
1727
|
-
me print
|
|
1726
|
+
me print Open's record of this agent, signed for (agent/me)
|
|
1728
1727
|
claim-invite
|
|
1729
1728
|
mint the claim link a wallet needs to claim this agent
|
|
1730
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({
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export { deriveAgentId, registerCanonicalPayload, registerPayloadDigest, type Re
|
|
|
3
3
|
export { canonicalIdentityPayload, nameAgent, parseIdentityFields, type AgentIdentityFields, type NameAgentArgs, } from "./identity.js";
|
|
4
4
|
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, type ActionProposal, type ArtifactReference, type JoinRequest, type ResumeRequest, type SessionContext, } from "./sessionWire.js";
|
|
5
5
|
export { encodeAckFrame, SESSION_ERROR_NAMES, MAX_PREDICTION_PACING_MS, admitAuthorityEvent, applyPredictionGateStatus, decodeAuthorityMessage, freshSessionBoundary, predictionGateMessage, predictionGatePreparationOf, predictionGateTargetReceipt, sessionErrorHint, sessionErrorName, PredictionGateConflictError, SessionBoundaryError, type AcceptedSessionBoundary, type AuthorityMessage, type PredictionGateMessage, type PredictionGateOpening, type PredictionGatePreparation, type PredictionGateRelease, type PredictionGateStatus, type PredictionWindowRef, type ReceiptRef, type ResumeCursor, type StateRef, type ViewSnapshot, } from "./sessionCodec.js";
|
|
6
|
-
export { type OpenTableView, type PlayReport, type SeatDecision, type SeatDecisionResult, type SeatPosition, SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.js";
|
|
6
|
+
export { type OpenTableView, type PlayReport, type SeatDecision, type SeatDecisionResult, type SeatPosition, SessionClient, SessionRefusal, chooseSeatAction, playSeat, waitForReadyOwner, } from "./session.js";
|
|
7
7
|
export { cardFromByte, decodeLegalActions, decodeParticipantView, encodeAction, pickAction, type Card, type Rank, type Suit, type TexasAction, type TexasLegalActions, type TexasSeatView, } from "./texas.js";
|
|
8
|
-
export {
|
|
8
|
+
export { buildConsentRequest, DEFAULT_OPEN_API_BASE_URL, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
|
|
9
9
|
export { PACKAGE_NAME, RELEASE_CHANNELS, channelDistTag, installCommand, installSpec, resolveChannel, versionMatchesChannel, type ReleaseChannel, } from "./channel.js";
|
|
10
10
|
export { fromHex, toHex } from "./bytes.js";
|
|
11
11
|
export { AGENT_HTTP_CAPABILITY_HEADER, AGENT_HTTP_CAPABILITY_NONCE_BYTES, MAX_AGENT_HTTP_CAPABILITY_WINDOW_MS, agentHttpCanonicalPayload, agentHttpSigningBytes, decodeAgentHttpHeader, encodeAgentHttpHeader, mintAgentHttpCapability, type AgentHttpBinding, type AgentHttpCapability, } from "./agentHttp.js";
|
|
@@ -17,5 +17,5 @@ export { handEquity, requiredEquity, cardCode, type HandEquity, type HandEquityO
|
|
|
17
17
|
export { cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, type BestFive, type HandCategory, } from "./handRank.js";
|
|
18
18
|
export { DEFAULT_SEAT_STATE_FILE, loadSeatState, saveSeatState, type SeatSessionState, } from "./seatState.js";
|
|
19
19
|
export { newSeatState, openTurn, submitTurn, type SeatStateCheckpoint, type SeatTurnOutcome, type SeatTurnPosition, } from "./seatTurn.js";
|
|
20
|
-
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, type JoinPlan, type SponsoredExecution, type TournamentAgentEntry, type TournamentPass, type TournamentPlay, type TournamentOwnerView, type TournamentOwnerAgent, type TournamentChainObjects, type TournamentClient, type TournamentOverview, type TournamentQueueEntry, } from "./openTournament.js";
|
|
21
|
-
export { ClientRefusal, describeNext, refusalLines, refusalMessage, refusalMessageFromText, type RefusalBody, type RefusalNext, } from "./refusal.js";
|
|
20
|
+
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readAgentEntrySettled, BOOK_CATCHES_UP_MS, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, type JoinPlan, type SponsoredExecution, type TournamentAgentEntry, type TournamentPass, type TournamentPlay, type TournamentOwnerView, type TournamentOwnerAgent, type TournamentChainObjects, type TournamentClient, type TournamentOverview, type TournamentQueueEntry, } from "./openTournament.js";
|
|
21
|
+
export { ClientRefusal, describeNext, ownershipFencePoll, ownershipFenced, refusalLines, refusalMessage, refusalMessageFromText, type RefusalBody, type RefusalNext, } from "./refusal.js";
|
package/dist/index.js
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Generate and hold an ed25519 keypair (Sui format, `.dopa-keypair`), produce
|
|
4
4
|
* the canonical signatures, and drive the Participant Session against
|
|
5
5
|
* `session_base_url`. Byte parity with the Rust client is pinned by
|
|
6
|
-
* `libs/dopa-open
|
|
6
|
+
* `libs/dopa-open/client-rs/vectors/ts-signer-parity.json`.
|
|
7
7
|
*/
|
|
8
8
|
export { DEFAULT_KEY_FILE, generateKeypair, keypairFromSeed, loadKeypair, saveKeypair, signOwnerAuthenticator, signRaw, } from "./keypair.js";
|
|
9
9
|
export { deriveAgentId, registerCanonicalPayload, registerPayloadDigest, } from "./registration.js";
|
|
10
10
|
export { canonicalIdentityPayload, nameAgent, parseIdentityFields, } from "./identity.js";
|
|
11
11
|
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, } from "./sessionWire.js";
|
|
12
12
|
export { encodeAckFrame, SESSION_ERROR_NAMES, MAX_PREDICTION_PACING_MS, admitAuthorityEvent, applyPredictionGateStatus, decodeAuthorityMessage, freshSessionBoundary, predictionGateMessage, predictionGatePreparationOf, predictionGateTargetReceipt, sessionErrorHint, sessionErrorName, PredictionGateConflictError, SessionBoundaryError, } from "./sessionCodec.js";
|
|
13
|
-
export { SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.js";
|
|
13
|
+
export { SessionClient, SessionRefusal, chooseSeatAction, playSeat, waitForReadyOwner, } from "./session.js";
|
|
14
14
|
export { cardFromByte, decodeLegalActions, decodeParticipantView, encodeAction, pickAction, } from "./texas.js";
|
|
15
|
-
export {
|
|
15
|
+
export { buildConsentRequest, DEFAULT_OPEN_API_BASE_URL, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
|
|
16
16
|
export { PACKAGE_NAME, RELEASE_CHANNELS, channelDistTag, installCommand, installSpec, resolveChannel, versionMatchesChannel, } from "./channel.js";
|
|
17
17
|
export { fromHex, toHex } from "./bytes.js";
|
|
18
18
|
export { AGENT_HTTP_CAPABILITY_HEADER, AGENT_HTTP_CAPABILITY_NONCE_BYTES, MAX_AGENT_HTTP_CAPABILITY_WINDOW_MS, agentHttpCanonicalPayload, agentHttpSigningBytes, decodeAgentHttpHeader, encodeAgentHttpHeader, mintAgentHttpCapability, } from "./agentHttp.js";
|
|
@@ -29,5 +29,5 @@ export { handEquity, requiredEquity, cardCode, } from "./equity.js";
|
|
|
29
29
|
export { cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, } from "./handRank.js";
|
|
30
30
|
export { DEFAULT_SEAT_STATE_FILE, loadSeatState, saveSeatState, } from "./seatState.js";
|
|
31
31
|
export { newSeatState, openTurn, submitTurn, } from "./seatTurn.js";
|
|
32
|
-
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, } from "./openTournament.js";
|
|
33
|
-
export { ClientRefusal, describeNext, refusalLines, refusalMessage, refusalMessageFromText, } from "./refusal.js";
|
|
32
|
+
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readAgentEntrySettled, BOOK_CATCHES_UP_MS, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, } from "./openTournament.js";
|
|
33
|
+
export { ClientRefusal, describeNext, ownershipFencePoll, ownershipFenced, refusalLines, refusalMessage, refusalMessageFromText, } from "./refusal.js";
|
package/dist/offer.js
CHANGED
|
@@ -52,7 +52,7 @@ function record(raw) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
export async function readOffer(productUrl, offerId) {
|
|
55
|
-
const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/
|
|
55
|
+
const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/v1/playground/matches/${offerId}`);
|
|
56
56
|
if (status !== 200)
|
|
57
57
|
throw new Error(refusalMessage("offer read", status, json));
|
|
58
58
|
return record(json);
|
|
@@ -81,7 +81,7 @@ export async function acceptOffer(productUrl, offerId, seat, agent) {
|
|
|
81
81
|
signature: Array.from(signed.signature),
|
|
82
82
|
seat_possession_signature: Array.from(signed.seatPossessionSignature),
|
|
83
83
|
});
|
|
84
|
-
const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/
|
|
84
|
+
const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/v1/playground/matches/${offerId}/acceptances`, { method: "POST", headers: { "content-type": "application/json" }, body });
|
|
85
85
|
if (status === 200 || status === 201)
|
|
86
86
|
return;
|
|
87
87
|
if (status === 409)
|
|
@@ -89,7 +89,7 @@ export async function acceptOffer(productUrl, offerId, seat, agent) {
|
|
|
89
89
|
throw new Error(refusalMessage("acceptance", status, json));
|
|
90
90
|
}
|
|
91
91
|
async function admit(productUrl, offerId, agentId, agent) {
|
|
92
|
-
const target = `/
|
|
92
|
+
const target = `/v1/playground/matches/${offerId}/admit`;
|
|
93
93
|
const { header } = await mintAgentHttpCapability(agent, agentId, {
|
|
94
94
|
method: "POST",
|
|
95
95
|
requestTarget: target,
|
package/dist/openTournament.d.ts
CHANGED
|
@@ -143,6 +143,28 @@ export declare function readTournamentMatches(productUrl: string, tournamentId:
|
|
|
143
143
|
export declare function disputeHolding(productUrl: string, tournamentId: string, chipAddress: string, fetchImpl?: typeof fetch): Promise<number | undefined>;
|
|
144
144
|
/** The agent's entry in the chip book, or `null` before its first redeem. */
|
|
145
145
|
export declare function readAgentEntry(productUrl: string, tournamentId: string, chipAddress: string, fetchImpl?: typeof fetch): Promise<TournamentAgentEntry | null>;
|
|
146
|
+
/** How long a book read waits for a join that has just landed.
|
|
147
|
+
*
|
|
148
|
+
* The book is the chain's, read a checkpoint behind it: an agent whose
|
|
149
|
+
* `queue_join` succeeded a moment ago is on chain and not yet in the book.
|
|
150
|
+
* Measured at about a second on a local stack; this is generous enough to
|
|
151
|
+
* cover a slower one and short enough that an agent that really is absent is
|
|
152
|
+
* told so rather than left waiting. */
|
|
153
|
+
export declare const BOOK_CATCHES_UP_MS = 8000;
|
|
154
|
+
/** The agent's entry, waited for while the book catches up with the chain.
|
|
155
|
+
*
|
|
156
|
+
* For a caller that has just been told its join landed. A plain
|
|
157
|
+
* `readAgentEntry` answering `null` in that window is not "this agent never
|
|
158
|
+
* queued" — it is "the book has not seen the queue join yet", and the two are
|
|
159
|
+
* worth telling apart before refusing somebody. Absent for the whole bound is
|
|
160
|
+
* the first answer, and this returns `null` for it. */
|
|
161
|
+
export declare function readAgentEntrySettled(productUrl: string, tournamentId: string, chipAddress: string, options?: {
|
|
162
|
+
boundMs?: number;
|
|
163
|
+
pollMs?: number;
|
|
164
|
+
fetchImpl?: typeof fetch;
|
|
165
|
+
sleep?: (ms: number) => Promise<void>;
|
|
166
|
+
now?: () => number;
|
|
167
|
+
}): Promise<TournamentAgentEntry | null>;
|
|
146
168
|
/** An owner's pass, or `null` when the owner has claimed none. */
|
|
147
169
|
export declare function readPass(productUrl: string, tournamentId: string, owner: string, fetchImpl?: typeof fetch): Promise<TournamentPass | null>;
|
|
148
170
|
/** An owner's side: its pass and every agent it has claimed, each with the
|
package/dist/openTournament.js
CHANGED
|
@@ -26,7 +26,7 @@ function base(productUrl) {
|
|
|
26
26
|
return productUrl.replace(/\/$/, "");
|
|
27
27
|
}
|
|
28
28
|
function tournamentPath(tournamentId, rest = "") {
|
|
29
|
-
return `/
|
|
29
|
+
return `/v1/tournaments/${tournamentId}${rest}`;
|
|
30
30
|
}
|
|
31
31
|
async function readJson(fetchImpl, url) {
|
|
32
32
|
const response = await fetchImpl(url);
|
|
@@ -57,7 +57,7 @@ export async function readTournament(productUrl, tournamentId, fetchImpl = fetch
|
|
|
57
57
|
}
|
|
58
58
|
/** The open-entry tournaments the product runs. */
|
|
59
59
|
export async function listTournaments(productUrl, fetchImpl = fetch) {
|
|
60
|
-
const { status, json } = await readJson(fetchImpl, `${base(productUrl)}/
|
|
60
|
+
const { status, json } = await readJson(fetchImpl, `${base(productUrl)}/v1/tournaments`);
|
|
61
61
|
if (status === 404)
|
|
62
62
|
return [];
|
|
63
63
|
if (status !== 200)
|
|
@@ -100,6 +100,37 @@ export async function readAgentEntry(productUrl, tournamentId, chipAddress, fetc
|
|
|
100
100
|
throw new TournamentRefusal(status, json, "chip book read");
|
|
101
101
|
return json;
|
|
102
102
|
}
|
|
103
|
+
/** How long a book read waits for a join that has just landed.
|
|
104
|
+
*
|
|
105
|
+
* The book is the chain's, read a checkpoint behind it: an agent whose
|
|
106
|
+
* `queue_join` succeeded a moment ago is on chain and not yet in the book.
|
|
107
|
+
* Measured at about a second on a local stack; this is generous enough to
|
|
108
|
+
* cover a slower one and short enough that an agent that really is absent is
|
|
109
|
+
* told so rather than left waiting. */
|
|
110
|
+
export const BOOK_CATCHES_UP_MS = 8_000;
|
|
111
|
+
/** The agent's entry, waited for while the book catches up with the chain.
|
|
112
|
+
*
|
|
113
|
+
* For a caller that has just been told its join landed. A plain
|
|
114
|
+
* `readAgentEntry` answering `null` in that window is not "this agent never
|
|
115
|
+
* queued" — it is "the book has not seen the queue join yet", and the two are
|
|
116
|
+
* worth telling apart before refusing somebody. Absent for the whole bound is
|
|
117
|
+
* the first answer, and this returns `null` for it. */
|
|
118
|
+
export async function readAgentEntrySettled(productUrl, tournamentId, chipAddress, options = {}) {
|
|
119
|
+
const boundMs = options.boundMs ?? BOOK_CATCHES_UP_MS;
|
|
120
|
+
const pollMs = options.pollMs ?? 1_000;
|
|
121
|
+
const now = options.now ?? Date.now;
|
|
122
|
+
const sleep = options.sleep ??
|
|
123
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
124
|
+
const deadline = now() + boundMs;
|
|
125
|
+
for (;;) {
|
|
126
|
+
const entry = await readAgentEntry(productUrl, tournamentId, chipAddress, options.fetchImpl ?? fetch);
|
|
127
|
+
if (entry)
|
|
128
|
+
return entry;
|
|
129
|
+
if (now() >= deadline)
|
|
130
|
+
return null;
|
|
131
|
+
await sleep(pollMs);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
103
134
|
/** An owner's pass, or `null` when the owner has claimed none. */
|
|
104
135
|
export async function readPass(productUrl, tournamentId, owner, fetchImpl = fetch) {
|
|
105
136
|
const { status, json } = await readJson(fetchImpl, `${base(productUrl)}${tournamentPath(tournamentId, `/passes/${owner}`)}`);
|
|
@@ -178,7 +209,7 @@ plays, owner) {
|
|
|
178
209
|
if (entry && entry.state !== "idle")
|
|
179
210
|
throw new JoinRefused(`this agent is already ${entry.state}${entry.state === "seated" ? " at a table" : ""}; an agent plays one table at a time`, {
|
|
180
211
|
action: "wait",
|
|
181
|
-
poll: `/
|
|
212
|
+
poll: `/v1/tournaments/${overview.tournamentId}/agents/${entry.chipAddress}`,
|
|
182
213
|
});
|
|
183
214
|
const booked = entry?.balance ?? 0;
|
|
184
215
|
const wallet = entry?.walletBalance ?? booked;
|
package/dist/refusal.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ export interface RefusalBody {
|
|
|
29
29
|
docs?: string;
|
|
30
30
|
}
|
|
31
31
|
/** `next …`, as one line an agent can act on. */
|
|
32
|
+
/** True when a refusal body says the writer is fenced and the caller should
|
|
33
|
+
* poll committed ownership rather than resend to the same origin. */
|
|
34
|
+
export declare function ownershipFenced(body: unknown): boolean;
|
|
35
|
+
/** The product route a fenced seat polls, when the refusal named one. */
|
|
36
|
+
export declare function ownershipFencePoll(body: unknown): string | undefined;
|
|
32
37
|
export declare function describeNext(next: RefusalNext): string;
|
|
33
38
|
/** The lines a refusal prints: its code, what it said, the move, the entry. */
|
|
34
39
|
export declare function refusalLines(body: unknown): string[];
|
package/dist/refusal.js
CHANGED
|
@@ -6,6 +6,27 @@
|
|
|
6
6
|
* reading a refused command's output reads its move off the same lines rather
|
|
7
7
|
* than off a table in a skill document. */
|
|
8
8
|
/** `next …`, as one line an agent can act on. */
|
|
9
|
+
/** True when a refusal body says the writer is fenced and the caller should
|
|
10
|
+
* poll committed ownership rather than resend to the same origin. */
|
|
11
|
+
export function ownershipFenced(body) {
|
|
12
|
+
if (typeof body !== "object" || body === null)
|
|
13
|
+
return false;
|
|
14
|
+
const refusal = body;
|
|
15
|
+
if (refusal.code !== "ownership_fenced")
|
|
16
|
+
return false;
|
|
17
|
+
return (isNext(refusal.next) &&
|
|
18
|
+
refusal.next.action === "wait" &&
|
|
19
|
+
typeof refusal.next.poll === "string");
|
|
20
|
+
}
|
|
21
|
+
/** The product route a fenced seat polls, when the refusal named one. */
|
|
22
|
+
export function ownershipFencePoll(body) {
|
|
23
|
+
if (!ownershipFenced(body))
|
|
24
|
+
return undefined;
|
|
25
|
+
const next = body.next;
|
|
26
|
+
if (next && next.action === "wait")
|
|
27
|
+
return next.poll;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
9
30
|
export function describeNext(next) {
|
|
10
31
|
switch (next.action) {
|
|
11
32
|
case "retry":
|
package/dist/room.js
CHANGED
|
@@ -57,7 +57,7 @@ export async function openRoom(client, seatCount) {
|
|
|
57
57
|
seatCount < MIN_ROOM_SEATS ||
|
|
58
58
|
seatCount > MAX_ROOM_SEATS)
|
|
59
59
|
throw new Error(`a private room seats ${MIN_ROOM_SEATS} to ${MAX_ROOM_SEATS}, not ${seatCount}`);
|
|
60
|
-
const { status, json } = await signedFetch(client, "POST", "/
|
|
60
|
+
const { status, json } = await signedFetch(client, "POST", "/v1/tables/private-rooms", { seatCount });
|
|
61
61
|
if (status !== 201) {
|
|
62
62
|
if (json?.code === "agent_not_claimed")
|
|
63
63
|
throw notClaimed();
|
|
@@ -95,7 +95,7 @@ export class RoomJoinRefusal extends Error {
|
|
|
95
95
|
}
|
|
96
96
|
/** Join a room by the id its opener handed out. */
|
|
97
97
|
export async function joinRoom(client, tableId) {
|
|
98
|
-
const target = `/
|
|
98
|
+
const target = `/v1/tables/private-rooms/${tableId}/join`;
|
|
99
99
|
const { status, json } = await signedFetch(client, "POST", target);
|
|
100
100
|
if (status !== 201) {
|
|
101
101
|
if (json?.code === "agent_not_claimed")
|
package/dist/seatState.d.ts
CHANGED
package/dist/seatState.js
CHANGED
|
@@ -175,6 +175,9 @@ export function encodeGate(gate) {
|
|
|
175
175
|
arrivalMs: gate.release.arrivalMs.toString(),
|
|
176
176
|
lockedAtMs: gate.release.lockedAtMs.toString(),
|
|
177
177
|
budgetMs: gate.release.budgetMs.toString(),
|
|
178
|
+
terminalDigest: gate.release.terminalDigest
|
|
179
|
+
? toHex0x(gate.release.terminalDigest)
|
|
180
|
+
: null,
|
|
178
181
|
},
|
|
179
182
|
};
|
|
180
183
|
}
|
|
@@ -208,6 +211,9 @@ export function decodeGate(stored) {
|
|
|
208
211
|
arrivalMs: BigInt(stored.release.arrivalMs),
|
|
209
212
|
lockedAtMs: BigInt(stored.release.lockedAtMs),
|
|
210
213
|
budgetMs: BigInt(stored.release.budgetMs),
|
|
214
|
+
terminalDigest: stored.release.terminalDigest
|
|
215
|
+
? fromHex(stored.release.terminalDigest)
|
|
216
|
+
: null,
|
|
211
217
|
}),
|
|
212
218
|
};
|
|
213
219
|
default:
|
package/dist/seatTurn.js
CHANGED
|
@@ -478,7 +478,7 @@ async function sayAtTable(fetchImpl, session, agent, agentId, say) {
|
|
|
478
478
|
if (!trimmed)
|
|
479
479
|
return false;
|
|
480
480
|
const { mintAgentHttpCapability, AGENT_HTTP_CAPABILITY_HEADER } = await import("./agentHttp.js");
|
|
481
|
-
const target = `/
|
|
481
|
+
const target = `/v1/executions/${session.executionHex}/talk`;
|
|
482
482
|
const body = new TextEncoder().encode(JSON.stringify({ say: trimmed }));
|
|
483
483
|
try {
|
|
484
484
|
const { header } = await mintAgentHttpCapability(agent, agentId, {
|
package/dist/session.d.ts
CHANGED
|
@@ -327,6 +327,18 @@ export interface RestartRetry {
|
|
|
327
327
|
* answer, 408, 429 or a 5xx -- until `bound` has passed. Any other answer is
|
|
328
328
|
* returned as it came, so a refusal stays the caller's to read. */
|
|
329
329
|
export declare function fetchWhileRestarting(fetchImpl: typeof fetch, url: string, retry?: RestartRetry): Promise<Response>;
|
|
330
|
+
/** Poll product until the committed owner is ready, then return its origin. */
|
|
331
|
+
export declare function waitForReadyOwner(args: {
|
|
332
|
+
productUrl: string;
|
|
333
|
+
executionId: string;
|
|
334
|
+
fetchImpl?: typeof fetch;
|
|
335
|
+
timeoutMs?: number;
|
|
336
|
+
pauseMs?: number;
|
|
337
|
+
sleep?: (ms: number) => Promise<void>;
|
|
338
|
+
}): Promise<{
|
|
339
|
+
sessionOrigin: string;
|
|
340
|
+
version: number;
|
|
341
|
+
}>;
|
|
330
342
|
export declare function openSeatSession(args: SeatSessionArgs): Promise<SeatSession>;
|
|
331
343
|
export declare function playSeat(args: PlayArgs): Promise<PlayReport>;
|
|
332
344
|
/** What the play loop does about a refusal, by the refusal's tag.
|
package/dist/session.js
CHANGED
|
@@ -7,6 +7,7 @@ import { MAX_TRANSPORT_FRAME_BYTES, PredictionGateConflictError, SessionBoundary
|
|
|
7
7
|
import { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, requireSessionVersion, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, } from "./sessionWire.js";
|
|
8
8
|
import { decodeLegalActions, decodeParticipantView, encodeAction, pickAction, } from "./texas.js";
|
|
9
9
|
import { authorizeSeatChallenge } from "./seatAuth.js";
|
|
10
|
+
import { ownershipFenced } from "./refusal.js";
|
|
10
11
|
const ACTION_IDENTITY_DOMAIN = textBytes("dopa_open::client::action_identity_v1");
|
|
11
12
|
/** The lines a play door reports for one seat's sitting.
|
|
12
13
|
*
|
|
@@ -528,13 +529,50 @@ export async function fetchWhileRestarting(fetchImpl, url, retry = {
|
|
|
528
529
|
await sleep(retry.pauseMs);
|
|
529
530
|
}
|
|
530
531
|
}
|
|
532
|
+
/** Poll product until the committed owner is ready, then return its origin. */
|
|
533
|
+
export async function waitForReadyOwner(args) {
|
|
534
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
535
|
+
const sleep = args.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
536
|
+
const product = args.productUrl.replace(/\/$/, "");
|
|
537
|
+
const until = Date.now() + (args.timeoutMs ?? 30_000);
|
|
538
|
+
const pause = args.pauseMs ?? 200;
|
|
539
|
+
const id = args.executionId.replace(/^0x/i, "");
|
|
540
|
+
for (;;) {
|
|
541
|
+
const response = await fetchImpl(`${product}/v1/authority/executions/${id}/route`);
|
|
542
|
+
if (response.ok) {
|
|
543
|
+
const body = (await response.json());
|
|
544
|
+
if (body.ready && body.sessionOrigin)
|
|
545
|
+
return { sessionOrigin: body.sessionOrigin, version: body.version ?? 0 };
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
await response.body?.cancel();
|
|
549
|
+
}
|
|
550
|
+
if (Date.now() + pause > until)
|
|
551
|
+
throw new Error("committed owner did not become ready");
|
|
552
|
+
await sleep(pause);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function httpFailure(error) {
|
|
556
|
+
if (!(error instanceof Error))
|
|
557
|
+
return null;
|
|
558
|
+
const match = /^[a-z-]+ failed \((\d+)\): (.*)$/is.exec(error.message);
|
|
559
|
+
if (!match)
|
|
560
|
+
return null;
|
|
561
|
+
const status = Number(match[1]);
|
|
562
|
+
try {
|
|
563
|
+
return { status, body: JSON.parse(match[2] ?? "") };
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return { status, body: match[2] ?? "" };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
531
569
|
export async function openSeatSession(args) {
|
|
532
570
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
533
571
|
const product = args.productUrl.replace(/\/$/, "");
|
|
534
572
|
/* Read again while the product is coming back: a seat reopens its session
|
|
535
573
|
after every dropped stream, and the product answering 502 for the seconds
|
|
536
574
|
it restarts ended the seat as surely as a refusal would have. */
|
|
537
|
-
const offer = await fetchWhileRestarting(fetchImpl, `${product}/
|
|
575
|
+
const offer = await fetchWhileRestarting(fetchImpl, `${product}/v1/playground/matches/${args.offerId}`, args.restartRetry);
|
|
538
576
|
if (!offer.ok)
|
|
539
577
|
throw new Error(`offer read failed (${offer.status}): ${await offer.text()}`);
|
|
540
578
|
const record = (await offer.json());
|
|
@@ -581,7 +619,7 @@ export async function openSeatSession(args) {
|
|
|
581
619
|
}
|
|
582
620
|
export async function playSeat(args) {
|
|
583
621
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
584
|
-
|
|
622
|
+
let { client, product, executionHex, coordinatorKey, timeAuthorityKey, executionId, } = await openSeatSession(args);
|
|
585
623
|
const record = {
|
|
586
624
|
admission: {
|
|
587
625
|
execution_id: executionId,
|
|
@@ -923,6 +961,22 @@ export async function playSeat(args) {
|
|
|
923
961
|
client joined this seat with this key; fighting it back would evict
|
|
924
962
|
a reconnect that may be the operator's own, so this one steps aside
|
|
925
963
|
and says so. Everything else is transient, and resume is right. */
|
|
964
|
+
const failed = httpFailure(error);
|
|
965
|
+
if (failed?.status === 409 && ownershipFenced(failed.body)) {
|
|
966
|
+
const ready = await waitForReadyOwner({
|
|
967
|
+
productUrl: product,
|
|
968
|
+
executionId: executionHex,
|
|
969
|
+
fetchImpl,
|
|
970
|
+
timeoutMs: 30_000,
|
|
971
|
+
pauseMs: 200,
|
|
972
|
+
});
|
|
973
|
+
client = new SessionClient(ready.sessionOrigin, args.agent, args.seat, args.agentId, client.executionId, client.executionManifestDigest, client.clientNonce, fetchImpl);
|
|
974
|
+
client.token = originToken;
|
|
975
|
+
await client.resume(lastContext, lastCursor);
|
|
976
|
+
originToken = client.token ?? originToken;
|
|
977
|
+
reconnects += 1;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
926
980
|
const answer = afterRefusal(error);
|
|
927
981
|
if (answer === "step-aside")
|
|
928
982
|
return {
|
|
@@ -1004,7 +1058,7 @@ export function normaliseCardCode(code) {
|
|
|
1004
1058
|
export async function readDisclosedEntitlement(fetchImpl, product, executionId, seat, capability, options = {}) {
|
|
1005
1059
|
const attempts = options.attempts ?? 15;
|
|
1006
1060
|
const pauseMs = options.pauseMs ?? 1_000;
|
|
1007
|
-
const target = `/
|
|
1061
|
+
const target = `/v1/spectator/executions/${executionId}`;
|
|
1008
1062
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1009
1063
|
try {
|
|
1010
1064
|
const header = capability ? await capability("GET", target) : null;
|
|
@@ -1032,7 +1086,7 @@ export async function readDisclosedEntitlement(fetchImpl, product, executionId,
|
|
|
1032
1086
|
* gone -- which is precisely the moment this question is being asked. */
|
|
1033
1087
|
export async function readSittingStatus(fetchImpl, product, executionHex) {
|
|
1034
1088
|
try {
|
|
1035
|
-
const response = await fetchImpl(`${product}/
|
|
1089
|
+
const response = await fetchImpl(`${product}/v1/history/matches/${executionHex}`);
|
|
1036
1090
|
if (!response.ok)
|
|
1037
1091
|
return { state: "unknown" };
|
|
1038
1092
|
const wire = (await response.json());
|
|
@@ -1073,7 +1127,7 @@ export function agentReadCapability(agent, agentId) {
|
|
|
1073
1127
|
export async function readPublicTable(fetchImpl, product, executionHex, names = new Map(), capability) {
|
|
1074
1128
|
/* Bound to the target the server reconstructs from the request, which is the
|
|
1075
1129
|
origin-form path and not the absolute URL the fetch is given. */
|
|
1076
|
-
const target = `/
|
|
1130
|
+
const target = `/v1/spectator/executions/${executionHex}`;
|
|
1077
1131
|
try {
|
|
1078
1132
|
const header = capability ? await capability("GET", target) : null;
|
|
1079
1133
|
const response = await fetchImpl(`${product}${target}`, header === null
|
|
@@ -1134,14 +1188,14 @@ export async function readPublicTable(fetchImpl, product, executionHex, names =
|
|
|
1134
1188
|
return null;
|
|
1135
1189
|
}
|
|
1136
1190
|
}
|
|
1137
|
-
/** What an agent is called, from `GET /
|
|
1191
|
+
/** What an agent is called, from `GET /v1/agents/{id}/custody`, read
|
|
1138
1192
|
* once and remembered in `names`. Null where the read did not answer. */
|
|
1139
1193
|
async function readAgentNaming(fetchImpl, product, agentId, names) {
|
|
1140
1194
|
const known = names.get(agentId);
|
|
1141
1195
|
if (known !== undefined)
|
|
1142
1196
|
return known;
|
|
1143
1197
|
try {
|
|
1144
|
-
const response = await fetchImpl(`${product}/
|
|
1198
|
+
const response = await fetchImpl(`${product}/v1/agents/${agentId}/custody`);
|
|
1145
1199
|
if (!response.ok) {
|
|
1146
1200
|
names.set(agentId, null);
|
|
1147
1201
|
return null;
|
|
@@ -1164,7 +1218,7 @@ async function readAgentNaming(fetchImpl, product, agentId, names) {
|
|
|
1164
1218
|
* table: an empty list where the read did not answer. */
|
|
1165
1219
|
export async function readTableTalk(fetchImpl, product, executionHex, handIndex) {
|
|
1166
1220
|
try {
|
|
1167
|
-
const response = await fetchImpl(`${product}/
|
|
1221
|
+
const response = await fetchImpl(`${product}/v1/executions/${executionHex}/talk`);
|
|
1168
1222
|
if (!response.ok)
|
|
1169
1223
|
return [];
|
|
1170
1224
|
const wire = (await response.json());
|
|
@@ -1200,7 +1254,7 @@ function potChips(pot) {
|
|
|
1200
1254
|
/** File one line at the table, signed as this seat's agent. True when the
|
|
1201
1255
|
* product accepted it. */
|
|
1202
1256
|
async function sayAtTable(fetchImpl, product, executionHex, args, say) {
|
|
1203
|
-
const target = `/
|
|
1257
|
+
const target = `/v1/executions/${executionHex}/talk`;
|
|
1204
1258
|
const body = textBytes(JSON.stringify({ say }));
|
|
1205
1259
|
try {
|
|
1206
1260
|
const { header } = await mintAgentHttpCapability(args.agent, args.agentId, {
|
package/dist/sessionCodec.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare const SEAT_AUTHORIZATION_RESPONSE_TAG = 16;
|
|
|
13
13
|
export declare const PREDICTION_GATE_RELEASED_TAG = 17;
|
|
14
14
|
export declare const PREDICTION_GATE_PREPARED_TAG = 18;
|
|
15
15
|
export declare const PREDICTION_GATE_OPENED_TAG = 19;
|
|
16
|
+
export declare const PREDICTION_GATE_RELEASED_V4_TAG = 20;
|
|
16
17
|
export declare const MAX_TRANSPORT_FRAME_BYTES: number;
|
|
17
18
|
export interface WireEnvelope {
|
|
18
19
|
message: string;
|
|
@@ -104,6 +105,9 @@ export interface PredictionGateRelease {
|
|
|
104
105
|
lockCappedMs: bigint;
|
|
105
106
|
budgetMs: bigint;
|
|
106
107
|
participantDeadlineMs: bigint;
|
|
108
|
+
/** Digest of the protected LLM terminal decision, when bound. A release
|
|
109
|
+
* with none is the version-three notice; a bound one is version four. */
|
|
110
|
+
terminalDigest: Uint8Array | null;
|
|
107
111
|
}
|
|
108
112
|
/** The gate facts a participant retains for one revision. */
|
|
109
113
|
export type PredictionGateStatus = {
|
package/dist/sessionCodec.js
CHANGED
|
@@ -23,6 +23,12 @@ export const SEAT_AUTHORIZATION_RESPONSE_TAG = 0x10;
|
|
|
23
23
|
export const PREDICTION_GATE_RELEASED_TAG = 0x11;
|
|
24
24
|
export const PREDICTION_GATE_PREPARED_TAG = 0x12;
|
|
25
25
|
export const PREDICTION_GATE_OPENED_TAG = 0x13;
|
|
26
|
+
/* The released notice carries a version per terminal-digest binding: a
|
|
27
|
+
release with no protected-terminal digest is the byte-identical
|
|
28
|
+
version-three notice, and one bound to a digest is this notice with the
|
|
29
|
+
raw 32-byte digest after the overlay. `arena_session::wire` picks the
|
|
30
|
+
same tag from the same fact. */
|
|
31
|
+
export const PREDICTION_GATE_RELEASED_V4_TAG = 0x14;
|
|
26
32
|
export const MAX_TRANSPORT_FRAME_BYTES = 1 << 20;
|
|
27
33
|
export function encodeAckFrame(context, cursor) {
|
|
28
34
|
const sequence = typeof cursor === "object" ? cursor.sequence : cursor;
|
|
@@ -276,7 +282,11 @@ export function bindPredictionGateOpening(fields) {
|
|
|
276
282
|
throw new Error("prediction gate closes at or before it opened");
|
|
277
283
|
return fields;
|
|
278
284
|
}
|
|
279
|
-
|
|
285
|
+
/* `bound` is the notice version, not a field the sender may vary: the
|
|
286
|
+
version-three notice ends at the overlay and names no digest, and the
|
|
287
|
+
version-four notice ends with the raw digest and no option byte. Reading
|
|
288
|
+
an option here would read the cursor that follows. */
|
|
289
|
+
function readPredictionGateRelease(reader, bound) {
|
|
280
290
|
const window = readPredictionWindow(reader);
|
|
281
291
|
const actingSeat = reader.readU16("prediction gate acting seat");
|
|
282
292
|
const state = readState(reader, "prediction gate state nonce", "prediction gate state commitment");
|
|
@@ -288,6 +298,9 @@ function readPredictionGateRelease(reader) {
|
|
|
288
298
|
const arrivalMs = reader.readU64("prediction gate arrival milliseconds");
|
|
289
299
|
const lockedAtMs = reader.readU64("prediction gate locked-at milliseconds");
|
|
290
300
|
const budgetMs = reader.readU64("participant deadline budget milliseconds");
|
|
301
|
+
const terminalDigest = bound
|
|
302
|
+
? reader.readFixed(32, "prediction gate terminal digest")
|
|
303
|
+
: null;
|
|
291
304
|
return bindPredictionGateRelease({
|
|
292
305
|
window,
|
|
293
306
|
actingSeat,
|
|
@@ -298,6 +311,7 @@ function readPredictionGateRelease(reader) {
|
|
|
298
311
|
arrivalMs,
|
|
299
312
|
lockedAtMs,
|
|
300
313
|
budgetMs,
|
|
314
|
+
terminalDigest,
|
|
301
315
|
});
|
|
302
316
|
}
|
|
303
317
|
/** The structural refusals of `PredictionGateRelease::new`, with the bounded
|
|
@@ -566,8 +580,9 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
566
580
|
};
|
|
567
581
|
break;
|
|
568
582
|
}
|
|
569
|
-
case PREDICTION_GATE_RELEASED_TAG:
|
|
570
|
-
|
|
583
|
+
case PREDICTION_GATE_RELEASED_TAG:
|
|
584
|
+
case PREDICTION_GATE_RELEASED_V4_TAG: {
|
|
585
|
+
const release = readPredictionGateRelease(reader, tag === PREDICTION_GATE_RELEASED_V4_TAG);
|
|
571
586
|
const cursor = readCursor(reader);
|
|
572
587
|
message = {
|
|
573
588
|
type: "predictionGateReleased",
|
package/dist/sessionWire.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Join, action, and resume signing bodies live here; acknowledgements are
|
|
5
5
|
* unsigned and encoded next to the authority decoder. The layouts are pinned
|
|
6
|
-
* by `libs/dopa-open
|
|
6
|
+
* by `libs/dopa-open/client-rs/vectors/ts-signer-parity.json`.
|
|
7
7
|
*/
|
|
8
8
|
import { ByteWriter, frameSigningBytes, textBytes } from "./bytes.js";
|
|
9
9
|
const JOIN_DOMAIN = textBytes("arena_session::join");
|
package/dist/settlement.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** The Product API's deployed default origin. `dopa-open`'s `--product-url`
|
|
2
|
+
* (and `DOPA_OPEN_PRODUCT_URL`) default here, matching the Rust client and
|
|
3
|
+
* CLI's `DEFAULT_OPEN_API_BASE_URL`, so a caller only has to override it for
|
|
4
|
+
* a local stack or a non-default plane. */
|
|
5
|
+
export declare const DEFAULT_OPEN_API_BASE_URL = "https://open.dopamint.fun";
|
|
1
6
|
export interface SeatEntitlement {
|
|
2
7
|
seat: number;
|
|
3
8
|
amount: bigint;
|
|
@@ -52,5 +57,4 @@ export declare function buildConsentRequest(args: ConsentDisclosure & {
|
|
|
52
57
|
signature: Uint8Array;
|
|
53
58
|
}): ConsentRequest;
|
|
54
59
|
export declare function settlementConsentPath(executionIdHex: string): string;
|
|
55
|
-
export declare function authorityOriginFromSessionBase(sessionBaseUrl: string): string;
|
|
56
60
|
export declare function digestForPrompt(prompt: ConsentPrompt): Uint8Array;
|
package/dist/settlement.js
CHANGED
|
@@ -2,6 +2,11 @@ import { ByteReader, ByteWriter, equalBytes, fromHex, textBytes, toHex0x, } from
|
|
|
2
2
|
import { digestFramed } from "./crypto.js";
|
|
3
3
|
const ANCHOR_DOMAIN_ID_V1 = textBytes("arena_tunnel::anchor_domain_id_v1");
|
|
4
4
|
const SETTLEMENT_PURPOSE = textBytes("arena_tunnel::settlement");
|
|
5
|
+
/** The Product API's deployed default origin. `dopa-open`'s `--product-url`
|
|
6
|
+
* (and `DOPA_OPEN_PRODUCT_URL`) default here, matching the Rust client and
|
|
7
|
+
* CLI's `DEFAULT_OPEN_API_BASE_URL`, so a caller only has to override it for
|
|
8
|
+
* a local stack or a non-default plane. */
|
|
9
|
+
export const DEFAULT_OPEN_API_BASE_URL = "https://open.dopamint.fun";
|
|
5
10
|
export function encodeTerminalState(state) {
|
|
6
11
|
const writer = new ByteWriter()
|
|
7
12
|
.pushU16(1)
|
|
@@ -103,13 +108,7 @@ export function buildConsentRequest(args) {
|
|
|
103
108
|
}
|
|
104
109
|
export function settlementConsentPath(executionIdHex) {
|
|
105
110
|
const hex = executionIdHex.replace(/^0x/, "");
|
|
106
|
-
return `/
|
|
107
|
-
}
|
|
108
|
-
export function authorityOriginFromSessionBase(sessionBaseUrl) {
|
|
109
|
-
const idx = sessionBaseUrl.indexOf("/v1/exec/");
|
|
110
|
-
if (idx <= 0)
|
|
111
|
-
throw new Error(`admission session_base_url is not an execution session URL: ${sessionBaseUrl}`);
|
|
112
|
-
return sessionBaseUrl.slice(0, idx);
|
|
111
|
+
return `/v1/authority/executions/${hex}/settlements`;
|
|
113
112
|
}
|
|
114
113
|
export function digestForPrompt(prompt) {
|
|
115
114
|
const terminal = decodeTerminalState(fromHex(prompt.encoded_terminal_state));
|
package/dist/tour.js
CHANGED
|
@@ -63,8 +63,8 @@ function unavailable(error) {
|
|
|
63
63
|
}
|
|
64
64
|
export async function enterTour(client, tour, floor = {}) {
|
|
65
65
|
const target = tour === "playground"
|
|
66
|
-
? "/
|
|
67
|
-
: "/
|
|
66
|
+
? "/v1/tours/playground/entries"
|
|
67
|
+
: "/v1/tours/tournament/entries";
|
|
68
68
|
if (floor.minAgents !== undefined && tour !== "playground")
|
|
69
69
|
throw new Error("a floor of real agents only applies to the playground");
|
|
70
70
|
/* No body at all when nothing is stated: the join every client sent before
|
|
@@ -124,13 +124,13 @@ export async function queueUntilSeated(client, tour, options = {}) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
export async function readPosition(client, tableId) {
|
|
127
|
-
const { status, json } = await signedFetch(client, "GET", `/
|
|
127
|
+
const { status, json } = await signedFetch(client, "GET", `/v1/tables/${tableId}/decision`);
|
|
128
128
|
if (status !== 200)
|
|
129
129
|
throw new Error(refusalMessage("decision read", status, json));
|
|
130
130
|
return json;
|
|
131
131
|
}
|
|
132
132
|
export async function act(client, tableId, move) {
|
|
133
|
-
const { status, json } = await signedFetch(client, "POST", `/
|
|
133
|
+
const { status, json } = await signedFetch(client, "POST", `/v1/tables/${tableId}/actions`, move);
|
|
134
134
|
if (status !== 200)
|
|
135
135
|
throw new Error(refusalMessage("action", status, json));
|
|
136
136
|
return json;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dopamint-fun/open-sdk",
|
|
3
|
-
"version": "0.2.0-dev.
|
|
3
|
+
"version": "0.2.0-dev.10",
|
|
4
4
|
"description": "DOPA-OPEN client SDK: generate and hold your own agent key, sign registrations, and play a Participant Session",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/CommandOSSLabs/dopamint-arena.git",
|
|
9
|
-
"directory": "libs/dopa-open
|
|
9
|
+
"directory": "libs/dopa-open/client-ts"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "dist/index.js",
|