@swarm-tips/client 0.2.10 → 0.2.12

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/SBOM.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@swarm-tips/client-0.2.10",
6
- "documentNamespace": "https://swarm.tips/sbom/client/0.2.10",
5
+ "name": "@swarm-tips/client-0.2.12",
6
+ "documentNamespace": "https://swarm.tips/sbom/client/0.2.12",
7
7
  "creationInfo": {
8
- "created": "2026-10-14T04:51:49.929Z",
8
+ "created": "2026-10-16T05:41:05.183Z",
9
9
  "creators": [
10
10
  "Tool: sdk/client/scripts/generate-sbom.mjs",
11
11
  "Organization: Swarm Tips"
@@ -15,8 +15,8 @@
15
15
  {
16
16
  "name": "@swarm-tips/client",
17
17
  "SPDXID": "SPDXRef-Package",
18
- "versionInfo": "0.2.10",
19
- "downloadLocation": "https://www.npmjs.com/package/@swarm-tips/client/v/0.2.10",
18
+ "versionInfo": "0.2.12",
19
+ "downloadLocation": "https://www.npmjs.com/package/@swarm-tips/client/v/0.2.12",
20
20
  "filesAnalyzed": false,
21
21
  "licenseConcluded": "MIT",
22
22
  "licenseDeclared": "MIT"
@@ -3153,7 +3153,7 @@ export type CoordinationGame = {
3153
3153
  },
3154
3154
  {
3155
3155
  "kind": "arg";
3156
- "path": "cert.match_id";
3156
+ "path": "cert_no_a.match_id";
3157
3157
  }
3158
3158
  ];
3159
3159
  };
@@ -914,10 +914,10 @@ export type Shillbot = {
914
914
  {
915
915
  "name": "agent";
916
916
  "docs": [
917
- "`task.agent` when `task.payout_to` is unset (zero); otherwise it must be",
918
- "`task.payout_to` (e.g. an extension-credit splitter vault). Reputation",
919
- "still accrues to `task.agent` (see `update_agent_stats`), so routing the",
920
- "payout never moves the agent's on-chain stats."
917
+ "`task.agent` when `task.payout_to` is unset (zero) or when the routed",
918
+ "`task.payout_to` vault is closed (proven via `remaining_accounts`);",
919
+ "otherwise it must be `task.payout_to`. Reputation still accrues to",
920
+ "`task.agent` (see `update_agent_stats`)."
921
921
  ];
922
922
  "writable": true;
923
923
  },
@@ -1241,10 +1241,8 @@ export type Shillbot = {
1241
1241
  "name": "agent";
1242
1242
  "docs": [
1243
1243
  "routing, mirroring `finalize_task`). Equals `task.agent` when",
1244
- "`task.payout_to` is unset; otherwise it must be `task.payout_to` (the",
1245
- "credit vault). Without this, a won challenge would pay the agent wallet",
1246
- "directly and bypass recoupment. Reputation/`total_challenges_lost` keys",
1247
- "off `task.agent` independently, so routing never moves on-chain stats."
1244
+ "`task.payout_to` is unset (or when `task.payout_to` is closed, proven",
1245
+ "via `remaining_accounts`); otherwise it must be `task.payout_to`."
1248
1246
  ];
1249
1247
  "writable": true;
1250
1248
  },
@@ -1375,7 +1373,8 @@ export type Shillbot = {
1375
1373
  {
1376
1374
  "name": "agent";
1377
1375
  "docs": [
1378
- "C2 routing override is set (same rule as finalize/resolve_challenge)."
1376
+ "C2 routing override is set (with fallback to `task.agent` if `payout_to`",
1377
+ "is closed, proven via `remaining_accounts`)."
1379
1378
  ];
1380
1379
  "writable": true;
1381
1380
  },
@@ -3147,7 +3147,7 @@
3147
3147
  },
3148
3148
  {
3149
3149
  "kind": "arg",
3150
- "path": "cert.match_id"
3150
+ "path": "cert_no_a.match_id"
3151
3151
  }
3152
3152
  ]
3153
3153
  }
@@ -908,10 +908,10 @@
908
908
  {
909
909
  "name": "agent",
910
910
  "docs": [
911
- "`task.agent` when `task.payout_to` is unset (zero); otherwise it must be",
912
- "`task.payout_to` (e.g. an extension-credit splitter vault). Reputation",
913
- "still accrues to `task.agent` (see `update_agent_stats`), so routing the",
914
- "payout never moves the agent's on-chain stats."
911
+ "`task.agent` when `task.payout_to` is unset (zero) or when the routed",
912
+ "`task.payout_to` vault is closed (proven via `remaining_accounts`);",
913
+ "otherwise it must be `task.payout_to`. Reputation still accrues to",
914
+ "`task.agent` (see `update_agent_stats`)."
915
915
  ],
916
916
  "writable": true
917
917
  },
@@ -1235,10 +1235,8 @@
1235
1235
  "name": "agent",
1236
1236
  "docs": [
1237
1237
  "routing, mirroring `finalize_task`). Equals `task.agent` when",
1238
- "`task.payout_to` is unset; otherwise it must be `task.payout_to` (the",
1239
- "credit vault). Without this, a won challenge would pay the agent wallet",
1240
- "directly and bypass recoupment. Reputation/`total_challenges_lost` keys",
1241
- "off `task.agent` independently, so routing never moves on-chain stats."
1238
+ "`task.payout_to` is unset (or when `task.payout_to` is closed, proven",
1239
+ "via `remaining_accounts`); otherwise it must be `task.payout_to`."
1242
1240
  ],
1243
1241
  "writable": true
1244
1242
  },
@@ -1369,7 +1367,8 @@
1369
1367
  {
1370
1368
  "name": "agent",
1371
1369
  "docs": [
1372
- "C2 routing override is set (same rule as finalize/resolve_challenge)."
1370
+ "C2 routing override is set (with fallback to `task.agent` if `payout_to`",
1371
+ "is closed, proven via `remaining_accounts`)."
1373
1372
  ],
1374
1373
  "writable": true
1375
1374
  },
@@ -10,10 +10,18 @@ const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
10
10
  const MEMO_PROGRAM_ID = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
11
11
  export const SESSION_FEE_HEADROOM_LAMPORTS = 10_000_000;
12
12
  export function sessionFundLamports(stakeLamports) {
13
- return Number(stakeLamports) + SESSION_FEE_HEADROOM_LAMPORTS;
13
+ const total = stakeLamports + BigInt(SESSION_FEE_HEADROOM_LAMPORTS);
14
+ if (total > BigInt(Number.MAX_SAFE_INTEGER)) {
15
+ throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.sessionFundLamports", message: "stakeLamports exceeds safe integer range" });
16
+ }
17
+ return Number(total);
14
18
  }
15
19
  export function sessionBalanceFloor(stakeLamports) {
16
- return Number(stakeLamports) + SESSION_FEE_HEADROOM_LAMPORTS / 2;
20
+ const total = stakeLamports + BigInt(SESSION_FEE_HEADROOM_LAMPORTS / 2);
21
+ if (total > BigInt(Number.MAX_SAFE_INTEGER)) {
22
+ throw new SwarmClientError({ code: "INVALID_ARGUMENT", operation: "solana.sessionBalanceFloor", message: "stakeLamports exceeds safe integer range" });
23
+ }
24
+ return Number(total);
17
25
  }
18
26
  function walletForSession(session) {
19
27
  return {
@@ -140,6 +148,13 @@ export class CoordinationGameSolanaClient {
140
148
  for (let index = 0; index < attempts; index += 1) {
141
149
  await sleep(options.intervalMs ?? 1_500);
142
150
  const status = await this.connection.getSignatureStatus(signature);
151
+ if (status.value?.err) {
152
+ throw new SwarmClientError({
153
+ code: "TRANSACTION_FAILED",
154
+ operation: "solana.waitForSignature",
155
+ message: `transaction ${signature} failed on-chain: ${JSON.stringify(status.value.err)}`,
156
+ });
157
+ }
143
158
  const confirmation = status.value?.confirmationStatus;
144
159
  if (confirmation === "confirmed" || confirmation === "finalized")
145
160
  return;
@@ -250,7 +265,14 @@ export class CoordinationGameSolanaClient {
250
265
  signed = await wallet.signTransaction(transaction);
251
266
  }
252
267
  const signature = await this.connection.sendRawTransaction(signed.serialize());
253
- await this.connection.confirmTransaction(signature, this.commitment);
268
+ const confirmed = await this.connection.confirmTransaction(signature, this.commitment);
269
+ if (confirmed?.value?.err) {
270
+ throw new SwarmClientError({
271
+ code: "TRANSACTION_FAILED",
272
+ operation: "solana.signBroadcastAndConfirm",
273
+ message: `transaction ${signature} failed on-chain: ${JSON.stringify(confirmed.value.err)}`,
274
+ });
275
+ }
254
276
  return signature;
255
277
  }
256
278
  async commitGuess(commitment, game, session) {
package/dist/http.js CHANGED
@@ -14,11 +14,13 @@ export class HttpClient {
14
14
  operation: "http.constructor",
15
15
  message: "A fetch implementation is required",
16
16
  });
17
- this.fetchImpl = candidate.bind(globalThis);
17
+ this.fetchImpl = options.fetch
18
+ ? (input, init) => options.fetch(input, init)
19
+ : candidate.bind(globalThis);
18
20
  this.getToken = options.getToken;
19
21
  }
20
22
  withNetwork(path) {
21
- if (!this.network || this.network === "mainnet")
23
+ if (!this.network || this.network === "mainnet" || /[?&]network=/.test(path))
22
24
  return path;
23
25
  const separator = path.includes("?") ? "&" : "?";
24
26
  return `${path}${separator}network=${encodeURIComponent(this.network)}`;
@@ -67,10 +67,21 @@ export function validatePreparedTransaction(encoded, expected) {
67
67
  const shillbotId = SHILLBOT_PROGRAM_ID.toBase58();
68
68
  const shillbotIxs = inspected.instructions.filter((instruction) => instruction.program_id === shillbotId);
69
69
  const lifecycle = shillbotIxs.filter((instruction) => matchesDiscriminator(instruction.data_base64, expected.action));
70
- if (lifecycle.length !== 1 || shillbotIxs.length !== 1) {
70
+ const companions = shillbotIxs.filter((instruction) => instruction !== lifecycle[0]);
71
+ const payoutDisc = shillbotInstructionDiscriminator("set_payout_to");
72
+ const validCompanions = companions.length === 0 ||
73
+ (expected.action === "claim" &&
74
+ companions.length === 1 &&
75
+ Buffer.from(companions[0].data_base64, "base64").subarray(0, 8).equals(payoutDisc) &&
76
+ inspected.instructions.indexOf(companions[0]) > inspected.instructions.indexOf(lifecycle[0]) &&
77
+ companions[0].accounts.length === 2 &&
78
+ (!expected.taskPda || companions[0].accounts[0] === expected.taskPda) &&
79
+ companions[0].accounts[1] === expected.wallet);
80
+ if (lifecycle.length !== 1 || !validCompanions) {
71
81
  throw new SwarmClientError({ code: "TRANSACTION_MISMATCH", operation: "shillbot.validatePreparedTransaction", message: "Expected exactly one matching Shillbot lifecycle instruction" });
72
82
  }
73
- if (expected.taskPda && !lifecycle[0].accounts.includes(expected.taskPda)) {
83
+ const taskAccountIndex = expected.action === "create" ? 1 : 0;
84
+ if (expected.taskPda && lifecycle[0].accounts[taskAccountIndex] !== expected.taskPda) {
74
85
  throw new SwarmClientError({ code: "TRANSACTION_MISMATCH", operation: "shillbot.validatePreparedTransaction", message: "Task PDA differs from expectation" });
75
86
  }
76
87
  const alwaysAllowed = new Set([
@@ -210,10 +210,25 @@ export function buildTransaction(request) {
210
210
  },
211
211
  };
212
212
  }
213
+ const COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
214
+ const SECP256K1_PROGRAM_ID = "KeccakSecp256k11111111111111111111111111111";
215
+ const SWITCHBOARD_PROGRAM_IDS = {
216
+ mainnet: "SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv",
217
+ devnet: "Aio4gaXjXzJNVLtzwtNVmSqGKpANtXhybbkhtAC94ji2",
218
+ };
213
219
  export function inspectTransaction(encoded) {
214
220
  const bytes = Buffer.from(encoded, "base64");
221
+ let tx;
215
222
  try {
216
- const tx = VersionedTransaction.deserialize(bytes);
223
+ tx = VersionedTransaction.deserialize(bytes);
224
+ }
225
+ catch {
226
+ tx = undefined;
227
+ }
228
+ if (tx) {
229
+ if (tx.message.addressTableLookups.length > 0) {
230
+ throw new Error("address lookup tables are not allowed in Shillbot lifecycle transactions");
231
+ }
217
232
  const keys = tx.message.staticAccountKeys;
218
233
  return {
219
234
  version: tx.version === "legacy" ? "legacy" : "v0",
@@ -228,22 +243,20 @@ export function inspectTransaction(encoded) {
228
243
  })),
229
244
  };
230
245
  }
231
- catch {
232
- const tx = Transaction.from(bytes);
233
- const message = tx.compileMessage();
234
- return {
235
- version: "legacy",
236
- fee_payer: message.accountKeys[0].toBase58(),
237
- signers: message.accountKeys
238
- .slice(0, message.header.numRequiredSignatures)
239
- .map((key) => key.toBase58()),
240
- instructions: message.instructions.map((ix) => ({
241
- program_id: message.accountKeys[ix.programIdIndex].toBase58(),
242
- accounts: ix.accounts.map((index) => message.accountKeys[index].toBase58()),
243
- data_base64: Buffer.from(bs58.decode(ix.data)).toString("base64"),
244
- })),
245
- };
246
- }
246
+ const legacyTx = Transaction.from(bytes);
247
+ const message = legacyTx.compileMessage();
248
+ return {
249
+ version: "legacy",
250
+ fee_payer: message.accountKeys[0].toBase58(),
251
+ signers: message.accountKeys
252
+ .slice(0, message.header.numRequiredSignatures)
253
+ .map((key) => key.toBase58()),
254
+ instructions: message.instructions.map((ix) => ({
255
+ program_id: message.accountKeys[ix.programIdIndex].toBase58(),
256
+ accounts: ix.accounts.map((index) => message.accountKeys[index].toBase58()),
257
+ data_base64: Buffer.from(bs58.decode(ix.data)).toString("base64"),
258
+ })),
259
+ };
247
260
  }
248
261
  export function verifyIntent(encoded, intent) {
249
262
  const { digest: _digest, ...rest } = intent;
@@ -255,14 +268,27 @@ export function verifyIntent(encoded, intent) {
255
268
  throw new Error("fee payer differs from intent");
256
269
  if (!inspected.signers.includes(intent.wallet))
257
270
  throw new Error("wallet is not a required signer");
271
+ const shillbotProgramId = SHILLBOT_PROGRAM_ID.toBase58();
272
+ const allowedPrograms = new Set([
273
+ shillbotProgramId,
274
+ COMPUTE_BUDGET_PROGRAM_ID,
275
+ ...(intent.action === "verify"
276
+ ? [SWITCHBOARD_PROGRAM_IDS[intent.network], SECP256K1_PROGRAM_ID]
277
+ : []),
278
+ ]);
279
+ for (const ix of inspected.instructions) {
280
+ if (!allowedPrograms.has(ix.program_id)) {
281
+ throw new Error(`unexpected companion program ${ix.program_id}`);
282
+ }
283
+ }
258
284
  const expectedDiscriminator = shillbotInstructionDiscriminator(intent.action === "submit" ? "submit_work" : `${intent.action}_task`);
259
- const lifecycle = inspected.instructions.filter((ix) => ix.program_id === SHILLBOT_PROGRAM_ID.toBase58() &&
285
+ const lifecycle = inspected.instructions.filter((ix) => ix.program_id === shillbotProgramId &&
260
286
  Buffer.from(ix.data_base64, "base64")
261
287
  .subarray(0, 8)
262
288
  .equals(expectedDiscriminator));
263
289
  if (lifecycle.length !== 1)
264
290
  throw new Error("expected exactly one Shillbot lifecycle instruction");
265
- const shillbotInstructions = inspected.instructions.filter((ix) => ix.program_id === SHILLBOT_PROGRAM_ID.toBase58());
291
+ const shillbotInstructions = inspected.instructions.filter((ix) => ix.program_id === shillbotProgramId);
266
292
  const payoutDiscriminator = shillbotInstructionDiscriminator("set_payout_to");
267
293
  const companions = shillbotInstructions.filter((ix) => ix !== lifecycle[0]);
268
294
  const lifecycleIndex = inspected.instructions.indexOf(lifecycle[0]);
@@ -289,6 +315,27 @@ export function verifyIntent(encoded, intent) {
289
315
  if (lifecycle[0].accounts.join(",") !== intent.accounts.join(",")) {
290
316
  throw new Error("lifecycle accounts differ from intent");
291
317
  }
318
+ if (intent.action === "create" &&
319
+ intent.arguments.escrowLamports !== undefined) {
320
+ const data = Buffer.from(lifecycle[0].data_base64, "base64");
321
+ if (data.length < 24)
322
+ throw new Error("create instruction data is truncated");
323
+ const view = new DataView(data.buffer, data.byteOffset + 16, 8);
324
+ if (view.getBigUint64(0, true) !== BigInt(String(intent.arguments.escrowLamports))) {
325
+ throw new Error("escrow amount differs from intent");
326
+ }
327
+ }
328
+ if (intent.action === "submit" &&
329
+ typeof intent.arguments.contentId === "string") {
330
+ const data = Buffer.from(lifecycle[0].data_base64, "base64");
331
+ if (data.length < 12)
332
+ throw new Error("submit instruction data is truncated");
333
+ const len = data.readUInt32LE(8);
334
+ if (data.length !== 12 + len ||
335
+ data.subarray(12, 12 + len).toString("utf8") !== intent.arguments.contentId) {
336
+ throw new Error("content id differs from intent");
337
+ }
338
+ }
292
339
  return inspected;
293
340
  }
294
341
  export async function signAndBroadcast(connection, encodedUnsigned, sign, confirmation) {
@@ -308,11 +355,11 @@ export async function signAndBroadcast(connection, encodedUnsigned, sign, confir
308
355
  verifySignatures: true,
309
356
  });
310
357
  const signature = await connection.sendRawTransaction(wire);
311
- if (confirmation) {
312
- await connection.confirmTransaction({ signature, ...confirmation }, "confirmed");
313
- }
314
- else {
315
- await connection.confirmTransaction(signature, "confirmed");
358
+ const confirmed = confirmation
359
+ ? await connection.confirmTransaction({ signature, ...confirmation }, "confirmed")
360
+ : await connection.confirmTransaction(signature, "confirmed");
361
+ if (confirmed?.value?.err) {
362
+ throw new Error(`transaction ${signature} failed on-chain: ${JSON.stringify(confirmed.value.err)}`);
316
363
  }
317
364
  return signature;
318
365
  }
@@ -55,5 +55,6 @@ export declare const resolveShillbotState: StateResolver;
55
55
  * §6 capture window: only Verified is attestable. Finalize closes the on-chain
56
56
  * account, so post-finalize the attestation is permanently unverifiable. */
57
57
  export declare const shillbotValidStates: (accountKind: string) => readonly string[];
58
+ export declare const SHILLBOT_VOW_PROGRAM_ID = "2tR37nqMpwdV4DVUHjzUmL1rH2DtkA8zrRA4EAhT7KMi";
58
59
  /** Convenience bundle for callers that just want the canonical Shillbot triple. */
59
60
  export declare const shillbotProtocol: ProtocolHandler;
@@ -56,10 +56,7 @@ export const decodeShillbotTask = (data, accountKind) => {
56
56
  const client = new PublicKey(data.slice(8, 40)).toBase58();
57
57
  const agent = new PublicKey(data.slice(40, 72)).toBase58();
58
58
  const state = data[72];
59
- // platform is at offset 73 but unused by the verifier (the wire
60
- // format carries it directly; verifier's field-equality check
61
- // covers it via the v1 §4 step 5 platform comparison done by the
62
- // caller, not here).
59
+ const platform = data[73];
63
60
  const content_hash = bytesToHex(data.slice(82, 114));
64
61
  const content_id_hash = bytesToHex(data.slice(114, 146));
65
62
  const composite_score = view.getBigUint64(162, true);
@@ -70,6 +67,7 @@ export const decodeShillbotTask = (data, accountKind) => {
70
67
  client,
71
68
  agent,
72
69
  state,
70
+ platform,
73
71
  composite_score,
74
72
  verified_at,
75
73
  verification_hash,
@@ -115,11 +113,13 @@ export const shillbotValidStates = (accountKind) => {
115
113
  return [];
116
114
  return ["verified"];
117
115
  };
116
+ export const SHILLBOT_VOW_PROGRAM_ID = "2tR37nqMpwdV4DVUHjzUmL1rH2DtkA8zrRA4EAhT7KMi";
118
117
  /** Convenience bundle for callers that just want the canonical Shillbot triple. */
119
118
  export const shillbotProtocol = {
120
119
  decode: decodeShillbotTask,
121
120
  resolveState: resolveShillbotState,
122
121
  validStates: shillbotValidStates,
122
+ programId: SHILLBOT_VOW_PROGRAM_ID,
123
123
  };
124
124
  function bytesToHex(b) {
125
125
  let s = "";
@@ -116,9 +116,12 @@ function isRfc3339NoFraction(v) {
116
116
  if (!pattern.test(v))
117
117
  return false;
118
118
  // Round-trip via Date to confirm the components describe a real
119
- // moment (rejects e.g. "2026-13-32T25:61:99Z").
119
+ // moment (rejects e.g. "2026-13-32T25:61:99Z" and rollover dates like "2026-02-31T00:00:00Z").
120
120
  const ms = Date.parse(v);
121
- return !Number.isNaN(ms);
121
+ if (Number.isNaN(ms))
122
+ return false;
123
+ const canonicalZ = v.replace(/\+00:00$/, "Z");
124
+ return new Date(ms).toISOString().replace(/\.\d+Z$/, "Z") === canonicalZ;
122
125
  }
123
126
  function isPlainObject(v) {
124
127
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -67,6 +67,7 @@ export interface DecodedAccount {
67
67
  verification_hash: string;
68
68
  content_hash: string;
69
69
  content_id_hash: string;
70
+ platform?: number;
70
71
  }
71
72
  /**
72
73
  * Per-protocol pluggable decoder. The verifier calls this with the
@@ -104,4 +105,5 @@ export interface ProtocolHandler {
104
105
  decode: AccountDecoder;
105
106
  resolveState: StateResolver;
106
107
  validStates: ValidStates;
108
+ programId?: string;
107
109
  }
@@ -72,6 +72,9 @@ export async function verifyV1OnChain(a, protocol, rpc) {
72
72
  if (accountInfo.owner.toBase58() !== a.program_id) {
73
73
  return "owner_mismatch";
74
74
  }
75
+ if (protocol.programId !== undefined && accountInfo.owner.toBase58() !== protocol.programId) {
76
+ return "owner_mismatch";
77
+ }
75
78
  // Step 4 — Anchor discriminator check.
76
79
  if (data.length < 8) {
77
80
  return "discriminator_mismatch";
@@ -103,6 +106,9 @@ export async function verifyV1OnChain(a, protocol, rpc) {
103
106
  if (decoded.agent !== a.agent) {
104
107
  return "field_mismatch:agent";
105
108
  }
109
+ if (decoded.platform !== undefined && decoded.platform !== a.platform) {
110
+ return "field_mismatch:platform";
111
+ }
106
112
  if (decoded.composite_score.toString() !== a.composite_score) {
107
113
  return "field_mismatch:composite_score";
108
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarm-tips/client",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Typed browser and Node.js clients for Swarm Tips, Shillbot, and Coordination Game",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",