@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 CHANGED
@@ -1,4 +1,4 @@
1
- # `libs/dopa-open-client-ts`
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-client-rs/vectors/ts-signer-parity.json`, generated by
40
- `libs/dopa-open-client-rs/tests/ts_parity_vectors.rs` and replayed by
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-api/vectors/agent_http_capability_v1.json`. A bearer token
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 readU64 = (offset) => {
98
- let out = 0n;
99
- for (let index = 0; index < 8; index++)
100
- out = (out << 8n) | BigInt(bytes[offset + index]);
101
- return out;
102
- };
103
- return {
104
- agentId: bytes.slice(0, 32),
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-client-rs/vectors/` pin all of it.
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 { concatBytes, equalBytes, frameSigningBytes, fromHex, textBytes, toHex0x, } from "./bytes.js";
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 concatBytes(AGENT_CLAIM_INVITE_DOMAIN, Uint8Array.of(0, CANONICAL_WIRE_VERSION, INVITE_OPERATION), fixed(invite.agentId, 32, "agent id"), invite.owner ? fixed(invite.owner, 32, "owner") : ZERO_OWNER, fixed(invite.agentPublicKey, 32, "agent public key"), u64be(invite.issuedAtMs), u64be(invite.expiresAtMs), fixed(invite.nonce, AGENT_CLAIM_INVITE_NONCE_BYTES, "nonce"));
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 concatBytes(fixed(invite.agentId, 32, "agent id"), invite.owner ?? ZERO_OWNER, fixed(invite.agentPublicKey, 32, "agent public key"), u64be(invite.issuedAtMs), u64be(invite.expiresAtMs), fixed(invite.nonce, 16, "nonce"), invite.signature);
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 view = new DataView(bytes.buffer, bytes.byteOffset);
119
- const owner = bytes.slice(32, 64);
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: bytes.slice(0, 32),
137
+ agentId,
122
138
  owner: equalBytes(owner, ZERO_OWNER) ? null : owner,
123
- agentPublicKey: bytes.slice(64, 96),
124
- issuedAtMs: view.getBigUint64(96, false),
125
- expiresAtMs: view.getBigUint64(104, false),
126
- nonce: bytes.slice(112, 128),
127
- signature: bytes.slice(128),
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(/\/$/, "")}/arena/agents/${id}/claim?invite=${token}`;
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 { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
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(/\/$/, "")}/open/v1/agents`, {
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}/open/v1/agents/${encodeURIComponent(agentId)}`);
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"]?.replace(/\/$/, "");
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}/open/v1/agents/${encodeURIComponent(agentId)}/${path}`, {
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(/\/$/, "")}/arena/matches/0x${record.executionId.replace(/^0x/i, "")}`;
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`: the arena's record of this agent, signed for.
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(/\/$/, "")}/open/v1/agents?owner=${ownerAddressHex}&limit=60`);
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 arena is claimed by ${ownerAddressHex}. The roster lists an agent under the wallet that ` +
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, "/open/v1/agent/me", agent, agentId);
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}/open/v1/agent/me`, response.status, body));
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(/\/$/, "")}/open/v1/playground/matches/${options.offerId}`);
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
- const origin = authorityOriginFromSessionBase(record.admission.session_base_url);
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(/\/$/, "")}/arena/agents/0x${values["agent-id"].replace(/^0x/i, "")}`);
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(/\/$/, "")}/arena/matches/0x${admitted.executionId.replace(/^0x/i, "")}`);
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(/\/$/, "")}/arena/tours/private-room/tables/${room.tableId}`);
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(/\/$/, "")}/arena/matches/0x${admitted.executionId.replace(/^0x/i, "")}`);
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: `/open/v1/tournaments/${tournamentId}/owners/${owner ?? "{owner}"}`,
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
- if (entry?.state !== "queued")
1475
- fail(`this agent is ${entry?.state ?? "not in the book"}; only a queued agent can leave, and a seated one plays its table out`);
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(/\/$/, "")}/arena/matches/${seated.executionId}`);
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 the arena's page; you, holding
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"]?.replace(/\/$/, "");
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
- /* The arena's pages and its API share an origin on a deployment; a local
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 the arena's record of this agent, signed for (agent/me)
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
@@ -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 bytes the owning address signs to name an agent. */
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 payload mirrors `AgentIdentityEdit::canonical_payload` in
10
- * `backend/dopa-open/product/src/domain/custodial/key_rotation.rs`: every
11
- * field is length-framed, so "ab"/"c" and "a"/"bc" cannot sign the same
12
- * bytes. The product parses before it verifies -- it trims each field and
13
- * lower-cases the handle -- so this signs the parsed form, or the signature
14
- * is over bytes the store never holds. */
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
- 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
- /** 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
- const parts = [parsed.name ?? "", parsed.handle ?? "", parsed.bio ?? ""].map((field) => encoder.encode(field));
46
- const chunks = [
47
- u64be(domain.length),
48
- domain,
49
- fromHex(edit.agentId),
50
- fromHex(edit.owner),
51
- ...parts.flatMap((field) => [u64be(field.length), field]),
52
- u64be(edit.issuedAtMs),
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;
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/i, "")}`;
77
- const response = await fetchImpl(`${args.productUrl.replace(/\/$/, "")}/open/v1/agents/${agentId}/identity`, {
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({