@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 +8 -1
- package/dist/acceptance.d.ts +6 -0
- package/dist/acceptance.js +34 -0
- package/dist/channel.d.ts +7 -6
- package/dist/channel.js +20 -16
- package/dist/claim.d.ts +19 -1
- package/dist/claim.js +47 -4
- package/dist/cli.js +866 -57
- package/dist/decide.d.ts +16 -0
- package/dist/decide.js +122 -0
- package/dist/equity.d.ts +34 -0
- package/dist/equity.js +99 -0
- package/dist/handRank.d.ts +36 -0
- package/dist/handRank.js +170 -0
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +14 -3
- package/dist/keypair.js +8 -1
- package/dist/offer.d.ts +7 -0
- package/dist/offer.js +38 -7
- package/dist/openTournament.d.ts +211 -0
- package/dist/openTournament.js +337 -0
- package/dist/refusal.d.ts +45 -0
- package/dist/refusal.js +84 -0
- package/dist/room.d.ts +66 -0
- package/dist/room.js +154 -0
- package/dist/seatState.d.ts +91 -0
- package/dist/seatState.js +137 -0
- package/dist/seatTurn.d.ts +138 -0
- package/dist/seatTurn.js +442 -0
- package/dist/session.d.ts +158 -5
- package/dist/session.js +365 -25
- package/dist/sessionCodec.d.ts +6 -0
- package/dist/sessionCodec.js +34 -3
- package/dist/sessionWire.d.ts +20 -0
- package/dist/sessionWire.js +44 -0
- package/dist/tour.d.ts +55 -4
- package/dist/tour.js +88 -11
- package/package.json +1 -1
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
|
-
- `
|
|
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
|
|
package/dist/acceptance.d.ts
CHANGED
|
@@ -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>;
|
package/dist/acceptance.js
CHANGED
|
@@ -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 = "
|
|
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
|
|
26
|
-
* — an install of a package that was not on the
|
|
27
|
-
* that read its skill: nobody had set the
|
|
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
|
-
*
|
|
37
|
-
*
|
|
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,
|
|
1
|
+
/* Release channels: one package name, two npm dist-tags.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
59
|
-
* — an install of a package that was not on the
|
|
60
|
-
* that read its skill: nobody had set the
|
|
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
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
|
66
|
-
|
|
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
|
|
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 =
|
|
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);
|