@dopamint-fun/open-sdk 0.1.0-dev.0 → 0.2.0-dev.0

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
@@ -24,7 +24,14 @@ signatures, not whether this package is used. Allowed dependencies:
24
24
  - `channel.ts` — the npm channel-to-install mapping every teaching surface
25
25
  reads (`prod` = bare install, `dev`/`canary` = dist tags, local tarball
26
26
  override).
27
- - `dopa-open` CLI `keygen`, `address`, `register`, `play`, `consent`, `sign`.
27
+ - `openTournament.ts` — the open-entry tournament on the agent's own key:
28
+ `readTournament` / `readAgentEntry` / `readBundle`, `planJoin` (which refuses
29
+ a join the chain would abort, and says why), `joinTransaction` /
30
+ `leaveTransaction`, `presentToTournament` (the possession proof matchmaking
31
+ needs), and `sponsorAndExecute`, which has the product pay the gas and
32
+ co-sign as gas owner.
33
+ - `dopa-open` CLI — `keygen`, `address`, `register`, `play`, `consent`, `sign`,
34
+ and `tournament status | join | leave`.
28
35
 
29
36
  ## Parity
30
37
 
@@ -16,9 +16,15 @@ export interface OfferAcceptance {
16
16
  acceptanceNonce: Uint8Array;
17
17
  }
18
18
  export declare function offerAcceptanceCanonicalPayload(acceptance: OfferAcceptance): Uint8Array;
19
+ export declare function localEvidenceHash(seat: number): Uint8Array;
20
+ /** The exact bytes a seat key signs to prove possession to the settlement
21
+ * registry on chain. */
22
+ export declare function seatPossessionMessage(seat: number): Uint8Array;
19
23
  export declare function offerAcceptanceSigningBytes(acceptance: OfferAcceptance): Uint8Array;
20
24
  export interface SignedOfferAcceptance extends OfferAcceptance {
21
25
  /** 64 bytes */
22
26
  signature: Uint8Array;
27
+ /** 64 bytes: this key over `seatPossessionMessage(seat)`. */
28
+ seatPossessionSignature: Uint8Array;
23
29
  }
24
30
  export declare function signOfferAcceptance(agent: AgentKeypair, acceptance: OfferAcceptance): Promise<SignedOfferAcceptance>;
@@ -13,6 +13,11 @@
13
13
  import { ByteWriter, frameSigningBytes, textBytes } from "./bytes.js";
14
14
  import { signRaw } from "./keypair.js";
15
15
  const OFFER_ACCEPTANCE_DOMAIN = textBytes("dopa_open::offer_acceptance::v1");
16
+ /* The settlement registry's possession domain, baked into the deployed
17
+ * `attestation_registry` Move package. Byte-identical to
18
+ * `SEAT_KEY_POSSESSION_DOMAIN` in `dopa_open_api`; `acceptance.test.ts` pins
19
+ * the whole message against a Rust-generated vector. */
20
+ const SEAT_KEY_POSSESSION_DOMAIN = textBytes("dopan180/key-possession-v1");
16
21
  const CANONICAL_WIRE_VERSION = 1;
17
22
  const OFFER_ACCEPTANCE_OPERATION = 5;
18
23
  export const ACCEPTANCE_NONCE_BYTES = 32;
@@ -37,12 +42,41 @@ export function offerAcceptanceCanonicalPayload(acceptance) {
37
42
  .pushFixed(acceptance.acceptanceNonce, ACCEPTANCE_NONCE_BYTES, "acceptance nonce")
38
43
  .bytes();
39
44
  }
45
+ /* The document the registry records for a seat: a tag byte, the seat index as
46
+ * a little-endian u64, zero-padded to 32. No timestamp and no nonce, so it is
47
+ * fixed for a seat - which is what lets the platform resubmit one signature
48
+ * later with a fresh document time instead of asking the agent again. */
49
+ export function localEvidenceHash(seat) {
50
+ if (!Number.isInteger(seat) || seat < 1 || seat > 0xff)
51
+ throw new RangeError("seat must be a 1-based byte index");
52
+ const out = new Uint8Array(32);
53
+ out[0] = 0x90;
54
+ new DataView(out.buffer).setBigUint64(1, BigInt(seat), true);
55
+ return out;
56
+ }
57
+ /** The exact bytes a seat key signs to prove possession to the settlement
58
+ * registry on chain. */
59
+ export function seatPossessionMessage(seat) {
60
+ const hash = localEvidenceHash(seat);
61
+ const out = new Uint8Array(SEAT_KEY_POSSESSION_DOMAIN.length + hash.length);
62
+ out.set(SEAT_KEY_POSSESSION_DOMAIN, 0);
63
+ out.set(hash, SEAT_KEY_POSSESSION_DOMAIN.length);
64
+ return out;
65
+ }
40
66
  export function offerAcceptanceSigningBytes(acceptance) {
41
67
  return frameSigningBytes(OFFER_ACCEPTANCE_DOMAIN, offerAcceptanceCanonicalPayload(acceptance));
42
68
  }
69
+ /* Two signatures by one key under two domains. The second is the artefact the
70
+ * on-chain settlement registry needs, and only this key can make it: the
71
+ * platform submits the registration and pays its gas, but `register_local`
72
+ * authorizes on this signature alone, so it can neither forge one nor aim one
73
+ * at another key. Sent at acceptance rather than at settlement because the
74
+ * tunnel funds at admit - a seat that turns out to be unregistrable after that
75
+ * has already committed the stake. */
43
76
  export async function signOfferAcceptance(agent, acceptance) {
44
77
  return {
45
78
  ...acceptance,
46
79
  signature: await signRaw(agent, offerAcceptanceSigningBytes(acceptance)),
80
+ seatPossessionSignature: await signRaw(agent, seatPossessionMessage(acceptance.seat)),
47
81
  };
48
82
  }
package/dist/channel.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const PACKAGE_NAME = "@dopamint-fun/open-sdk";
2
- export type ReleaseChannel = "canary" | "dev" | "prod";
2
+ export type ReleaseChannel = "dev" | "prod";
3
3
  export declare const RELEASE_CHANNELS: readonly ReleaseChannel[];
4
4
  /** The npm dist-tag a channel publishes under and installs from. */
5
5
  export declare function channelDistTag(channel: ReleaseChannel): string;
@@ -22,9 +22,10 @@ export declare function resolveChannel(raw: string | undefined): ReleaseChannel;
22
22
  *
23
23
  * `resolveChannel` treats an unset value as prod on purpose: the bare install
24
24
  * is the safe default for a deployment that never says otherwise. That default
25
- * is also how the canary stack came to serve `npm install @dopamint-fun/open-sdk`
26
- * — an install of a package that was not on the registry — to every agent
27
- * that read its skill: nobody had set the variable, and nothing said so.
25
+ * is also how a non-production stack came to serve `npm install
26
+ * @dopamint-fun/open-sdk` — an install of a package that was not on the
27
+ * registry — to every agent that read its skill: nobody had set the
28
+ * variable, and nothing said so.
28
29
  *
29
30
  * So a deployment that names a non-production environment has to name a
30
31
  * channel too. Production keeps the default, because there the default is
@@ -33,6 +34,6 @@ export declare function resolveChannel(raw: string | undefined): ReleaseChannel;
33
34
  export declare function resolveDeployedChannel(channelRaw: string | undefined, deployEnvironment: string | undefined): ReleaseChannel;
34
35
  /** Whether a package version belongs on a channel - what the publish script
35
36
  * enforces before it will pass `--tag`. Prod takes stable semver only; dev
36
- * and canary take only their own prerelease identifier, so a tag and a
37
- * version can never disagree about what a build is. */
37
+ * takes only its own prerelease identifier, so a tag and a version can never
38
+ * disagree about what a build is. */
38
39
  export declare function versionMatchesChannel(version: string, channel: ReleaseChannel): boolean;
package/dist/channel.js CHANGED
@@ -1,9 +1,16 @@
1
- /* Release channels: one package name, three npm dist-tags.
1
+ /* Release channels: one package name, two npm dist-tags.
2
2
  *
3
- * canary, dev and prod are the same package at different maturities, selected
4
- * by tag rather than by name - a name suffix would fork the install line and
5
- * every document teaching it. Prod rides `latest` so the bare install is
6
- * production; the other two are opt-in by tag.
3
+ * dev and prod are the same package at two maturities, selected by tag rather
4
+ * than by name - a name suffix would fork the install line and every document
5
+ * teaching it. Prod rides `latest` so the bare install is production; dev is
6
+ * opt-in by tag.
7
+ *
8
+ * Two channels, two environments, one branch each: `dev` ships from `dev`,
9
+ * `prod` ships from `main`. There was a third, `canary`, and it is gone. A
10
+ * channel is a promise that some version is published under that tag, and
11
+ * nobody was keeping it: that stack spent months teaching an install line for
12
+ * a tag that held nothing. Anything that is not production teaches `dev`, so
13
+ * another stack needs no third name here.
7
14
  *
8
15
  * This module is the single owner of the channel-to-install mapping. The
9
16
  * skill serving plugin, the playground connect panel, and the SDK skill all
@@ -11,11 +18,7 @@
11
18
  * another elsewhere.
12
19
  */
13
20
  export const PACKAGE_NAME = "@dopamint-fun/open-sdk";
14
- export const RELEASE_CHANNELS = [
15
- "canary",
16
- "dev",
17
- "prod",
18
- ];
21
+ export const RELEASE_CHANNELS = ["dev", "prod"];
19
22
  /** The npm dist-tag a channel publishes under and installs from. */
20
23
  export function channelDistTag(channel) {
21
24
  return channel === "prod" ? "latest" : channel;
@@ -55,9 +58,10 @@ export function resolveChannel(raw) {
55
58
  *
56
59
  * `resolveChannel` treats an unset value as prod on purpose: the bare install
57
60
  * is the safe default for a deployment that never says otherwise. That default
58
- * is also how the canary stack came to serve `npm install @dopamint-fun/open-sdk`
59
- * — an install of a package that was not on the registry — to every agent
60
- * that read its skill: nobody had set the variable, and nothing said so.
61
+ * is also how a non-production stack came to serve `npm install
62
+ * @dopamint-fun/open-sdk` — an install of a package that was not on the
63
+ * registry — to every agent that read its skill: nobody had set the
64
+ * variable, and nothing said so.
61
65
  *
62
66
  * So a deployment that names a non-production environment has to name a
63
67
  * channel too. Production keeps the default, because there the default is
@@ -68,15 +72,15 @@ export function resolveDeployedChannel(channelRaw, deployEnvironment) {
68
72
  if (environment && environment !== "production" && !channelRaw?.trim()) {
69
73
  throw new Error(`DOPA_OPEN_SDK_CHANNEL is unset on the ${environment} deployment. Unset ` +
70
74
  `means prod, and a ${environment} stack teaching the production install ` +
71
- `line is how canary served an install nobody could run. Set it to one ` +
75
+ `line is how a stack came to serve an install nobody could run. Set it to one ` +
72
76
  `of ${RELEASE_CHANNELS.join(", ")} on the dopamint-arena-${environment} environment.`);
73
77
  }
74
78
  return resolveChannel(channelRaw);
75
79
  }
76
80
  /** Whether a package version belongs on a channel - what the publish script
77
81
  * enforces before it will pass `--tag`. Prod takes stable semver only; dev
78
- * and canary take only their own prerelease identifier, so a tag and a
79
- * version can never disagree about what a build is. */
82
+ * takes only its own prerelease identifier, so a tag and a version can never
83
+ * disagree about what a build is. */
80
84
  export function versionMatchesChannel(version, channel) {
81
85
  const stable = /^\d+\.\d+\.\d+$/;
82
86
  if (channel === "prod")
package/dist/claim.d.ts CHANGED
@@ -32,8 +32,26 @@ export declare function mintClaimInvite(agent: AgentKeypair, agentId: Uint8Array
32
32
  ttlMs?: number;
33
33
  nonce?: Uint8Array;
34
34
  }): Promise<ClaimInvite>;
35
- /** The token the link carries and the claim body posts back: 0x-hex. */
35
+ /** The token the link carries and the claim body posts back: 0x-hex.
36
+ *
37
+ * Kept because it is what already-minted links and the Rust vectors are
38
+ * written in. New links take the base64url form below, which is the same
39
+ * bytes 130 characters shorter. */
36
40
  export declare function encodeClaimInvite(invite: ClaimInvite): string;
41
+ /** The same token, base64url, which is what a link should carry.
42
+ *
43
+ * A claim link is pasted into chat, a terminal and a browser bar, and hex
44
+ * spends two characters on every byte to say it in an alphabet nobody reads.
45
+ * 192 bytes is 386 characters of `0x`-hex and 256 of base64url, and the
46
+ * saving is the whole of the difference between a link and a wall of text.
47
+ * Nothing signed changes: this is the encoding of the token, not the token.
48
+ *
49
+ * base64url specifically, not base64: `+` and `/` are not safe in a query
50
+ * string, and `=` padding is dropped because a fixed-width token needs none
51
+ * to be read back. */
52
+ export declare function encodeClaimInviteCompact(invite: ClaimInvite): string;
53
+ /** Read either encoding. A link minted before the compact form still works,
54
+ * and a reader never has to be told which one it is holding. */
37
55
  export declare function decodeClaimInvite(token: string): ClaimInvite;
38
56
  /** Where the owner goes to accept: the agent's claim page with the token. */
39
57
  export declare function claimInviteLink(arenaOrigin: string, agentIdHex: string, token: string): string;
package/dist/claim.js CHANGED
@@ -62,14 +62,57 @@ export async function mintClaimInvite(agent, agentId, options = {}) {
62
62
  invite.signature = await signRaw(agent, claimInviteSigningBytes(invite));
63
63
  return invite;
64
64
  }
65
- /** The token the link carries and the claim body posts back: 0x-hex. */
66
- export function encodeClaimInvite(invite) {
65
+ /** The 192 bytes a token carries, in order. */
66
+ function claimInviteBytes(invite) {
67
67
  if (invite.signature.length !== 64)
68
68
  throw new Error("an invitation is encoded only once it is signed");
69
- return toHex0x(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));
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
+ }
71
+ /** The token the link carries and the claim body posts back: 0x-hex.
72
+ *
73
+ * Kept because it is what already-minted links and the Rust vectors are
74
+ * written in. New links take the base64url form below, which is the same
75
+ * bytes 130 characters shorter. */
76
+ export function encodeClaimInvite(invite) {
77
+ return toHex0x(claimInviteBytes(invite));
78
+ }
79
+ /** The same token, base64url, which is what a link should carry.
80
+ *
81
+ * A claim link is pasted into chat, a terminal and a browser bar, and hex
82
+ * spends two characters on every byte to say it in an alphabet nobody reads.
83
+ * 192 bytes is 386 characters of `0x`-hex and 256 of base64url, and the
84
+ * saving is the whole of the difference between a link and a wall of text.
85
+ * Nothing signed changes: this is the encoding of the token, not the token.
86
+ *
87
+ * base64url specifically, not base64: `+` and `/` are not safe in a query
88
+ * string, and `=` padding is dropped because a fixed-width token needs none
89
+ * to be read back. */
90
+ export function encodeClaimInviteCompact(invite) {
91
+ return Buffer.from(claimInviteBytes(invite))
92
+ .toString("base64")
93
+ .replace(/\+/g, "-")
94
+ .replace(/\//g, "_")
95
+ .replace(/=+$/, "");
96
+ }
97
+ /** The bytes behind a token, whichever alphabet it arrived in.
98
+ *
99
+ * Told apart by shape, not by a flag: hex is 384 characters of `[0-9a-f]`
100
+ * with an optional `0x`, and anything else is read as base64url. A caller
101
+ * who mistypes one gets a length error from the decoder rather than a wrong
102
+ * invitation, because both forms are fixed width. */
103
+ function claimInviteTokenBytes(token) {
104
+ const bare = token.startsWith("0x") || token.startsWith("0X")
105
+ ? token.slice(2)
106
+ : token;
107
+ if (/^[0-9a-fA-F]+$/.test(bare) && bare.length % 2 === 0)
108
+ return fromHex(bare);
109
+ const padded = bare.replace(/-/g, "+").replace(/_/g, "/");
110
+ return new Uint8Array(Buffer.from(padded, "base64"));
70
111
  }
112
+ /** Read either encoding. A link minted before the compact form still works,
113
+ * and a reader never has to be told which one it is holding. */
71
114
  export function decodeClaimInvite(token) {
72
- const bytes = fromHex(token.trim());
115
+ const bytes = claimInviteTokenBytes(token.trim());
73
116
  if (bytes.length !== AGENT_CLAIM_INVITE_TOKEN_BYTES)
74
117
  throw new Error(`an invitation token is ${AGENT_CLAIM_INVITE_TOKEN_BYTES} bytes, got ${bytes.length}`);
75
118
  const view = new DataView(bytes.buffer, bytes.byteOffset);