@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/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 { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
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-client-rs/vectors/ts-signer-parity.json`.
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 { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
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(/\/$/, "")}/open/v1/playground/matches/${offerId}`);
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(/\/$/, "")}/open/v1/playground/matches/${offerId}/acceptances`, { method: "POST", headers: { "content-type": "application/json" }, body });
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 = `/open/v1/playground/matches/${offerId}/admit`;
92
+ const target = `/v1/playground/matches/${offerId}/admit`;
93
93
  const { header } = await mintAgentHttpCapability(agent, agentId, {
94
94
  method: "POST",
95
95
  requestTarget: target,
@@ -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
@@ -26,7 +26,7 @@ function base(productUrl) {
26
26
  return productUrl.replace(/\/$/, "");
27
27
  }
28
28
  function tournamentPath(tournamentId, rest = "") {
29
- return `/open/v1/tournaments/${tournamentId}${rest}`;
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)}/open/v1/tournaments`);
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: `/open/v1/tournaments/${overview.tournamentId}/agents/${entry.chipAddress}`,
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", "/open/v1/tables/private-rooms", { seatCount });
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 = `/open/v1/tables/private-rooms/${tableId}/join`;
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")
@@ -100,6 +100,7 @@ interface SerialisedRelease {
100
100
  arrivalMs: string;
101
101
  lockedAtMs: string;
102
102
  budgetMs: string;
103
+ terminalDigest: string | null;
103
104
  }
104
105
  type SerialisedGate = {
105
106
  phase: "prepared";
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 = `/open/v1/executions/${session.executionHex}/talk`;
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}/open/v1/playground/matches/${args.offerId}`, args.restartRetry);
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
- const { client, product, executionHex, coordinatorKey, timeAuthorityKey, executionId, } = await openSeatSession(args);
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 = `/open/v1/spectator/executions/${executionId}`;
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}/open/v1/history/matches/${executionHex}`);
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 = `/open/v1/spectator/executions/${executionHex}`;
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 /open/v1/agents/{id}/custody`, read
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}/open/v1/agents/${agentId}/custody`);
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}/open/v1/executions/${executionHex}/talk`);
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 = `/open/v1/executions/${executionHex}/talk`;
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, {
@@ -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 = {
@@ -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
- function readPredictionGateRelease(reader) {
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
- const release = readPredictionGateRelease(reader);
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",
@@ -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-client-rs/vectors/ts-signer-parity.json`.
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");
@@ -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;
@@ -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 `/open/v1/authority/executions/${hex}/settlements`;
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
- ? "/open/v1/tours/playground/entries"
67
- : "/open/v1/tours/tournament/entries";
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", `/open/v1/tables/${tableId}/decision`);
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", `/open/v1/tables/${tableId}/actions`, move);
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.1",
3
+ "version": "0.2.0-dev.11",
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-client-ts"
9
+ "directory": "libs/dopa-open/client-ts"
10
10
  },
11
11
  "type": "module",
12
12
  "main": "dist/index.js",