@dopamint-fun/open-sdk 0.2.0-dev.1 → 0.2.0-dev.11
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.d.ts +9 -0
- package/dist/bytes.js +18 -1
- package/dist/claim.js +38 -22
- package/dist/cli.js +53 -54
- package/dist/identity.d.ts +1 -1
- package/dist/identity.js +20 -40
- 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.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ export declare function toHex0x(bytes: Uint8Array): string;
|
|
|
4
4
|
/** Length-aware equality. `every()` on an empty left-hand side is vacuously
|
|
5
5
|
* true, which would accept a truncated hex decode as a matching key. */
|
|
6
6
|
export declare function equalBytes(left: Uint8Array, right: Uint8Array): boolean;
|
|
7
|
+
/** One `label:value` line per field, under a line naming the domain.
|
|
8
|
+
*
|
|
9
|
+
* What an owner's wallet is asked to approve is text, not a frame: a Sui
|
|
10
|
+
* wallet renders a personal message as characters, and a framed payload
|
|
11
|
+
* reached a person as NUL bytes and junk they could not check. The order of
|
|
12
|
+
* the fields is signed, and so is the absence of a trailing newline. */
|
|
13
|
+
export declare function signedMessage(domain: string, fields: readonly (readonly [label: string, value: string])[]): Uint8Array;
|
|
14
|
+
/** An id or address at the one width the message renders it, or a throw. */
|
|
15
|
+
export declare function fixedHex0x(value: string, length: number, field: string): string;
|
|
7
16
|
/** An append-only byte buffer with the wire's own vocabulary. */
|
|
8
17
|
export declare class ByteWriter {
|
|
9
18
|
private chunks;
|
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 = "";
|
|
@@ -37,6 +37,23 @@ export function equalBytes(left, right) {
|
|
|
37
37
|
return false;
|
|
38
38
|
return left.every((byte, index) => byte === right[index]);
|
|
39
39
|
}
|
|
40
|
+
/** One `label:value` line per field, under a line naming the domain.
|
|
41
|
+
*
|
|
42
|
+
* What an owner's wallet is asked to approve is text, not a frame: a Sui
|
|
43
|
+
* wallet renders a personal message as characters, and a framed payload
|
|
44
|
+
* reached a person as NUL bytes and junk they could not check. The order of
|
|
45
|
+
* the fields is signed, and so is the absence of a trailing newline. */
|
|
46
|
+
export function signedMessage(domain, fields) {
|
|
47
|
+
const lines = fields.map(([label, value]) => `${label}:${value}`);
|
|
48
|
+
return textBytes([domain, ...lines].join("\n"));
|
|
49
|
+
}
|
|
50
|
+
/** An id or address at the one width the message renders it, or a throw. */
|
|
51
|
+
export function fixedHex0x(value, length, field) {
|
|
52
|
+
const bytes = fromHex(value);
|
|
53
|
+
if (bytes.length !== length)
|
|
54
|
+
throw new Error(`${field} must be ${length} bytes, got ${bytes.length}`);
|
|
55
|
+
return toHex0x(bytes);
|
|
56
|
+
}
|
|
40
57
|
/** An append-only byte buffer with the wire's own vocabulary. */
|
|
41
58
|
export class ByteWriter {
|
|
42
59
|
chunks = [];
|
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.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface AgentIdentityFields {
|
|
|
9
9
|
}
|
|
10
10
|
/** The form the product stores, which is the form the signature covers. */
|
|
11
11
|
export declare function parseIdentityFields(fields: AgentIdentityFields): Required<AgentIdentityFields>;
|
|
12
|
-
/** The exact
|
|
12
|
+
/** The exact message the owning address signs to name an agent. */
|
|
13
13
|
export declare function canonicalIdentityPayload(edit: {
|
|
14
14
|
agentId: string;
|
|
15
15
|
owner: string;
|
package/dist/identity.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* and names it. After a claim the same route belongs to the claiming wallet,
|
|
7
7
|
* and this key can no longer sign for it -- which is the point of a claim.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
* `backend/dopa-open/product/src/domain/custodial/key_rotation.rs`:
|
|
11
|
-
* field is
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* The message mirrors `AgentIdentityEdit::canonical_message` in
|
|
10
|
+
* `backend/dopa-open/product/src/domain/custodial/key_rotation.rs`: the two
|
|
11
|
+
* ids render at a fixed 32 bytes and every named field is quoted, so "ab"/"c"
|
|
12
|
+
* and "a"/"bc" cannot sign the same text and a newline inside a name cannot
|
|
13
|
+
* forge a line of its own. The product parses before it verifies -- it trims
|
|
14
|
+
* each field and lower-cases the handle -- so this signs the parsed form, or
|
|
15
|
+
* the signature is over text the store never holds. */
|
|
16
|
+
import { fixedHex0x, signedMessage } 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,18 @@ export function parseIdentityFields(fields) {
|
|
|
25
27
|
bio: fields.bio?.trim() || null,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
|
-
|
|
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
|
-
/** The exact bytes the owning address signs to name an agent. */
|
|
30
|
+
/** The exact message the owning address signs to name an agent. */
|
|
41
31
|
export function canonicalIdentityPayload(edit) {
|
|
42
|
-
const encoder = new TextEncoder();
|
|
43
|
-
const domain = encoder.encode(IDENTITY_DOMAIN);
|
|
44
32
|
const parsed = parseIdentityFields(edit.fields);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
60
|
-
}
|
|
61
|
-
return out;
|
|
33
|
+
return signedMessage(IDENTITY_DOMAIN, [
|
|
34
|
+
["agent", fixedHex0x(edit.agentId, 32, "agent id")],
|
|
35
|
+
["owner", fixedHex0x(edit.owner, 32, "owner")],
|
|
36
|
+
["name", JSON.stringify(parsed.name ?? "")],
|
|
37
|
+
["handle", JSON.stringify(parsed.handle ?? "")],
|
|
38
|
+
["bio", JSON.stringify(parsed.bio ?? "")],
|
|
39
|
+
["issued_at_ms", String(edit.issuedAtMs)],
|
|
40
|
+
["expires_at_ms", String(edit.expiresAtMs)],
|
|
41
|
+
]);
|
|
62
42
|
}
|
|
63
43
|
/** Name an agent, as the address that owns it. Answers what the arena stored. */
|
|
64
44
|
export async function nameAgent(args) {
|
|
@@ -73,8 +53,8 @@ export async function nameAgent(args) {
|
|
|
73
53
|
issuedAtMs,
|
|
74
54
|
expiresAtMs,
|
|
75
55
|
}));
|
|
76
|
-
const agentId = `0x${args.agentId.replace(/^0x
|
|
77
|
-
const response = await fetchImpl(`${args.productUrl.replace(/\/$/, "")}/
|
|
56
|
+
const agentId = `0x${args.agentId.replace(/^0x/, "")}`;
|
|
57
|
+
const response = await fetchImpl(`${args.productUrl.replace(/\/$/, "")}/v1/agents/${agentId}/identity`, {
|
|
78
58
|
method: "PUT",
|
|
79
59
|
headers: { "content-type": "application/json" },
|
|
80
60
|
body: JSON.stringify({
|