@bitsocial/pubsub-voting 0.0.6

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.
Files changed (119) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +223 -0
  3. package/dist/chain/bucket.d.ts +13 -0
  4. package/dist/chain/bucket.js +24 -0
  5. package/dist/chain/coalescer.d.ts +56 -0
  6. package/dist/chain/coalescer.js +217 -0
  7. package/dist/chain/ticker.d.ts +15 -0
  8. package/dist/chain/ticker.js +25 -0
  9. package/dist/chain/types.d.ts +90 -0
  10. package/dist/chain/types.js +1 -0
  11. package/dist/checkpoint/codec.d.ts +54 -0
  12. package/dist/checkpoint/codec.js +99 -0
  13. package/dist/client/root-puller.d.ts +49 -0
  14. package/dist/client/root-puller.js +140 -0
  15. package/dist/client/voter.d.ts +239 -0
  16. package/dist/client/voter.js +1343 -0
  17. package/dist/crdt/codec.d.ts +41 -0
  18. package/dist/crdt/codec.js +137 -0
  19. package/dist/crdt/crdt.d.ts +22 -0
  20. package/dist/crdt/crdt.js +127 -0
  21. package/dist/crdt/store.d.ts +8 -0
  22. package/dist/crdt/store.js +23 -0
  23. package/dist/crdt/types.d.ts +87 -0
  24. package/dist/crdt/types.js +1 -0
  25. package/dist/encoding/canonical.d.ts +22 -0
  26. package/dist/encoding/canonical.js +26 -0
  27. package/dist/errors.d.ts +81 -0
  28. package/dist/errors.js +126 -0
  29. package/dist/index.d.ts +32 -0
  30. package/dist/index.js +43 -0
  31. package/dist/rules/constant.d.ts +14 -0
  32. package/dist/rules/constant.js +18 -0
  33. package/dist/rules/erc20-balance.d.ts +30 -0
  34. package/dist/rules/erc20-balance.js +44 -0
  35. package/dist/rules/erc721-min-balance.d.ts +18 -0
  36. package/dist/rules/erc721-min-balance.js +100 -0
  37. package/dist/rules/registry.d.ts +42 -0
  38. package/dist/rules/registry.js +61 -0
  39. package/dist/rules/types.d.ts +74 -0
  40. package/dist/rules/types.js +1 -0
  41. package/dist/schema/common.d.ts +25 -0
  42. package/dist/schema/common.js +24 -0
  43. package/dist/schema/criteria.d.ts +78 -0
  44. package/dist/schema/criteria.js +76 -0
  45. package/dist/schema/directory.d.ts +42 -0
  46. package/dist/schema/directory.js +52 -0
  47. package/dist/schema/votes.d.ts +55 -0
  48. package/dist/schema/votes.js +116 -0
  49. package/dist/signer/eip712.d.ts +103 -0
  50. package/dist/signer/eip712.js +85 -0
  51. package/dist/signer/types.d.ts +31 -0
  52. package/dist/signer/types.js +1 -0
  53. package/dist/storage/browser.d.ts +3 -0
  54. package/dist/storage/browser.js +110 -0
  55. package/dist/storage/memory.d.ts +10 -0
  56. package/dist/storage/memory.js +56 -0
  57. package/dist/storage/node.d.ts +5 -0
  58. package/dist/storage/node.js +106 -0
  59. package/dist/storage/types.d.ts +46 -0
  60. package/dist/storage/types.js +1 -0
  61. package/dist/store/indexeddb.d.ts +9 -0
  62. package/dist/store/indexeddb.js +72 -0
  63. package/dist/store/memory.d.ts +15 -0
  64. package/dist/store/memory.js +22 -0
  65. package/dist/store/select.d.ts +15 -0
  66. package/dist/store/select.js +64 -0
  67. package/dist/store/sqlite.d.ts +11 -0
  68. package/dist/store/sqlite.js +68 -0
  69. package/dist/store/types.d.ts +57 -0
  70. package/dist/store/types.js +1 -0
  71. package/dist/tally/tally.d.ts +44 -0
  72. package/dist/tally/tally.js +89 -0
  73. package/dist/tally/types.d.ts +51 -0
  74. package/dist/tally/types.js +13 -0
  75. package/dist/topic.d.ts +20 -0
  76. package/dist/topic.js +28 -0
  77. package/dist/transport/accepted-dedup.d.ts +30 -0
  78. package/dist/transport/accepted-dedup.js +34 -0
  79. package/dist/transport/announce/browser.d.ts +9 -0
  80. package/dist/transport/announce/browser.js +14 -0
  81. package/dist/transport/announce/node.d.ts +38 -0
  82. package/dist/transport/announce/node.js +162 -0
  83. package/dist/transport/announce/types.d.ts +74 -0
  84. package/dist/transport/announce/types.js +16 -0
  85. package/dist/transport/bundle-store.d.ts +11 -0
  86. package/dist/transport/bundle-store.js +34 -0
  87. package/dist/transport/chase.d.ts +125 -0
  88. package/dist/transport/chase.js +182 -0
  89. package/dist/transport/gossip-validator.d.ts +107 -0
  90. package/dist/transport/gossip-validator.js +99 -0
  91. package/dist/transport/helia.d.ts +55 -0
  92. package/dist/transport/helia.js +114 -0
  93. package/dist/transport/integration/harness.d.ts +65 -0
  94. package/dist/transport/integration/harness.js +278 -0
  95. package/dist/transport/messages.d.ts +97 -0
  96. package/dist/transport/messages.js +117 -0
  97. package/dist/transport/rate-limit.d.ts +11 -0
  98. package/dist/transport/rate-limit.js +20 -0
  99. package/dist/transport/transport.d.ts +20 -0
  100. package/dist/transport/transport.js +35 -0
  101. package/dist/transport/types.d.ts +189 -0
  102. package/dist/transport/types.js +1 -0
  103. package/dist/verify/background.d.ts +81 -0
  104. package/dist/verify/background.js +236 -0
  105. package/dist/verify/bundle.d.ts +58 -0
  106. package/dist/verify/bundle.js +84 -0
  107. package/dist/verify/cache.d.ts +48 -0
  108. package/dist/verify/cache.js +62 -0
  109. package/dist/verify/constraints.d.ts +16 -0
  110. package/dist/verify/constraints.js +35 -0
  111. package/dist/verify/gate-result-cache.d.ts +65 -0
  112. package/dist/verify/gate-result-cache.js +91 -0
  113. package/dist/verify/name-resolution-cache.d.ts +59 -0
  114. package/dist/verify/name-resolution-cache.js +64 -0
  115. package/dist/verify/signature.d.ts +9 -0
  116. package/dist/verify/signature.js +55 -0
  117. package/dist/verify/types.d.ts +101 -0
  118. package/dist/verify/types.js +1 -0
  119. package/package.json +78 -0
@@ -0,0 +1,91 @@
1
+ const memKeyFor = (wallet, sampleBlock) => `${wallet.toLowerCase()}:${sampleBlock}`;
2
+ /**
3
+ * An in-memory {@link GateResultCache} bounded to `maxEntries` with FIFO eviction. Eviction is
4
+ * safe because a score is deterministic — an evicted entry only ever costs a re-read, never a
5
+ * wrong answer; without a bound, a flood of fresh wallets is a memory-exhaustion vector (see
6
+ * DESIGN.md "Can valid votes clog the topic?").
7
+ */
8
+ export function makeGateResultCache(maxEntries = 4096) {
9
+ const byKey = new Map();
10
+ const order = [];
11
+ return {
12
+ get: async (wallet, sampleBlock) => byKey.get(memKeyFor(wallet, sampleBlock)),
13
+ set: (wallet, sampleBlock, score) => {
14
+ const k = memKeyFor(wallet, sampleBlock);
15
+ if (byKey.has(k))
16
+ return; // idempotent: never refresh position or overwrite a pinned score
17
+ byKey.set(k, score);
18
+ order.push(k);
19
+ if (order.length > maxEntries) {
20
+ const evicted = order.shift();
21
+ if (evicted !== undefined)
22
+ byKey.delete(evicted);
23
+ }
24
+ }
25
+ };
26
+ }
27
+ /** The persistent gate store's key. `ruleHash` disambiguates: the shared store spans every
28
+ * contest on the voter, and one wallet can hold different scores under different gate rules
29
+ * (or the same rule at different chainIds). Same score under the same rule is what lets two
30
+ * contests over one gate (a 5chan-style directory) share each other's reads. */
31
+ const storeKeyFor = (ruleHash, wallet, sampleBlock) => `${ruleHash}:${wallet.toLowerCase()}:${sampleBlock}`;
32
+ /**
33
+ * A {@link GateResultCache} layered over the voter's persistent store: an in-memory FIFO front
34
+ * (the hot path — steady-state gossip hits it synchronously) with read-through to the store on
35
+ * a miss and fire-and-forget write-through on `set`. Scores travel as decimal strings (JSON has
36
+ * no bigint). A broken store read or write degrades to a live chain read — never an error into
37
+ * the verify pipeline — because everything here is a pure function of pinned historical state.
38
+ */
39
+ export function makePersistentGateResultCache(opts) {
40
+ const { store, ruleHash } = opts;
41
+ const mem = makeGateResultCache(opts.maxMemEntries);
42
+ return {
43
+ async get(wallet, sampleBlock) {
44
+ const cached = await mem.get(wallet, sampleBlock);
45
+ if (cached !== undefined)
46
+ return cached;
47
+ let persisted;
48
+ try {
49
+ persisted = await store.getItem(storeKeyFor(ruleHash, wallet, sampleBlock));
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ if (typeof persisted !== "string" || !/^\d+$/.test(persisted))
55
+ return undefined;
56
+ const score = BigInt(persisted);
57
+ mem.set(wallet, sampleBlock, score);
58
+ return score;
59
+ },
60
+ set(wallet, sampleBlock, score) {
61
+ mem.set(wallet, sampleBlock, score);
62
+ void store.setItem(storeKeyFor(ruleHash, wallet, sampleBlock), score.toString()).catch(() => {
63
+ // a failed persist costs a future re-read, never a wrong answer
64
+ });
65
+ }
66
+ };
67
+ }
68
+ /**
69
+ * Deterministic expiry purge for one rule's persisted gate results — better than LRU here
70
+ * because staleness is *provable*: a score at bucket B's sample block is only ever consulted
71
+ * while bundles from B are admissible (within `voteExpiryBuckets` of head), so anything older
72
+ * than the oldest admissible sample block can never be read again. Run per contest whenever a
73
+ * head read advances the expiry boundary (see the engine's `#maybePurgeGateResults`); the
74
+ * store's LRU bound stays as the backstop for rules never purged.
75
+ */
76
+ export async function purgeExpiredGateResults(opts) {
77
+ const prefix = `${opts.ruleHash}:`;
78
+ try {
79
+ for (const key of await opts.store.keys()) {
80
+ if (!key.startsWith(prefix))
81
+ continue;
82
+ const sampleBlock = Number(key.slice(key.lastIndexOf(":") + 1));
83
+ if (Number.isFinite(sampleBlock) && sampleBlock < opts.oldestSampleBlock) {
84
+ await opts.store.removeItem(key);
85
+ }
86
+ }
87
+ }
88
+ catch {
89
+ // purge is best-effort; the store's LRU bound is the correctness-free backstop
90
+ }
91
+ }
@@ -0,0 +1,59 @@
1
+ import type { NameResolver } from "../chain/types.js";
2
+ import type { LruStorage } from "../storage/types.js";
3
+ /**
4
+ * Persistent cache of community-name resolutions, a port of pkc-js's `NameResolutionCache`
5
+ * (clients/name-resolution-cache.ts) — the SAME rule, so a host reasons about one caching
6
+ * policy across both libraries:
7
+ *
8
+ * - LRU-bounded persistent store, no stored TTL: an entry lives until evicted.
9
+ * - Freshness is a per-CALL max-age, modeled on HTTP `Cache-Control: max-age` (RFC 9111
10
+ * §5.2.1.1): `maxAgeSeconds: 0` bypasses the cache, `N` serves an entry only while
11
+ * `Date.now() - resolvedAtMs <= N * 1000`, `undefined` serves whatever is cached.
12
+ * - Only SUCCESSFUL resolutions are stored; a name that fails to resolve is retried by the
13
+ * caller, never negatively cached. No stale-while-revalidate.
14
+ * - Keyed `{name}::{resolverKey}::{sha256(provider)}` — per resolver identity, because two
15
+ * resolvers (or providers) may legitimately disagree during a migration.
16
+ *
17
+ * This cache exists because the host's own pkc-js instance caches at ITS call sites, not
18
+ * inside the injected resolver — so without this layer every verify here pays a live
19
+ * registry read that pkc-js would have served from cache.
20
+ */
21
+ export interface NameResolutionCacheEntry {
22
+ publicKey: string;
23
+ resolverKey: string;
24
+ provider: string;
25
+ resolvedAtMs: number;
26
+ }
27
+ export interface NameResolutionCache {
28
+ get(args: {
29
+ name: string;
30
+ resolverKey: string;
31
+ provider: string;
32
+ maxAgeSeconds?: number;
33
+ }): Promise<NameResolutionCacheEntry | undefined>;
34
+ set(args: {
35
+ name: string;
36
+ resolverKey: string;
37
+ provider: string;
38
+ publicKey: string;
39
+ }): Promise<void>;
40
+ }
41
+ /**
42
+ * The verify pipeline's freshness bound, matching pkc-js's background resolution call site
43
+ * (`cache: { maxAge: 3600 }`): a re-pointed name is honored here within at most one hour.
44
+ * Names gate vote validity, so this is the staleness ceiling a community re-point can see.
45
+ */
46
+ export declare const NAME_RESOLUTION_MAX_AGE_SECONDS = 3600;
47
+ export declare function makeNameResolutionCache(store: LruStorage): NameResolutionCache;
48
+ /**
49
+ * The verify pipeline's single resolution path (inline forward-gate AND background verifier):
50
+ * serve from the cache within {@link NAME_RESOLUTION_MAX_AGE_SECONDS}, otherwise resolve live
51
+ * and persist a success. `undefined` (no record) is returned uncached — the pkc-js rule.
52
+ */
53
+ export declare function resolveNameThroughCache(opts: {
54
+ resolver: NameResolver;
55
+ name: string;
56
+ cache: NameResolutionCache | undefined;
57
+ }): Promise<{
58
+ publicKey: string;
59
+ } | undefined>;
@@ -0,0 +1,64 @@
1
+ import { sha256, stringToHex } from "viem";
2
+ /**
3
+ * The verify pipeline's freshness bound, matching pkc-js's background resolution call site
4
+ * (`cache: { maxAge: 3600 }`): a re-pointed name is honored here within at most one hour.
5
+ * Names gate vote validity, so this is the staleness ceiling a community re-point can see.
6
+ */
7
+ export const NAME_RESOLUTION_MAX_AGE_SECONDS = 3600;
8
+ const keyFor = (args) => `${args.name}::${args.resolverKey}::${sha256(stringToHex(args.provider))}`;
9
+ export function makeNameResolutionCache(store) {
10
+ return {
11
+ async get(args) {
12
+ if (args.maxAgeSeconds === 0)
13
+ return undefined;
14
+ let entry;
15
+ try {
16
+ entry = await store.getItem(keyFor(args));
17
+ }
18
+ catch {
19
+ return undefined; // a broken cache read degrades to a live resolution, never a failure
20
+ }
21
+ if (entry === null ||
22
+ typeof entry !== "object" ||
23
+ typeof entry.publicKey !== "string" ||
24
+ typeof entry.resolvedAtMs !== "number") {
25
+ return undefined;
26
+ }
27
+ const cached = entry;
28
+ if (typeof args.maxAgeSeconds === "number" && Date.now() - cached.resolvedAtMs > args.maxAgeSeconds * 1000) {
29
+ return undefined;
30
+ }
31
+ return cached;
32
+ },
33
+ async set(args) {
34
+ const entry = {
35
+ publicKey: args.publicKey,
36
+ resolverKey: args.resolverKey,
37
+ provider: args.provider,
38
+ resolvedAtMs: Date.now()
39
+ };
40
+ try {
41
+ await store.setItem(keyFor(args), entry);
42
+ }
43
+ catch {
44
+ // a failed cache write costs a future re-resolution, never a wrong answer
45
+ }
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * The verify pipeline's single resolution path (inline forward-gate AND background verifier):
51
+ * serve from the cache within {@link NAME_RESOLUTION_MAX_AGE_SECONDS}, otherwise resolve live
52
+ * and persist a success. `undefined` (no record) is returned uncached — the pkc-js rule.
53
+ */
54
+ export async function resolveNameThroughCache(opts) {
55
+ const { resolver, name, cache } = opts;
56
+ const identity = { name, resolverKey: resolver.key, provider: resolver.provider };
57
+ const cached = await cache?.get({ ...identity, maxAgeSeconds: NAME_RESOLUTION_MAX_AGE_SECONDS });
58
+ if (cached)
59
+ return { publicKey: cached.publicKey };
60
+ const record = await resolver.resolve({ name });
61
+ if (record && cache)
62
+ await cache.set({ ...identity, publicKey: record.publicKey });
63
+ return record;
64
+ }
@@ -0,0 +1,9 @@
1
+ import type { VotesBundle } from "../schema/votes.js";
2
+ import type { OfflineBundleVerifier, VerifyResult } from "./types.js";
3
+ export declare function verifyBundleSignature(args: {
4
+ bundle: VotesBundle;
5
+ criteriaCid: Uint8Array;
6
+ chainId: number;
7
+ }): Promise<VerifyResult>;
8
+ /** The {@link OfflineBundleVerifier} seam, backed by {@link verifyBundleSignature}. */
9
+ export declare const offlineBundleVerifier: OfflineBundleVerifier;
@@ -0,0 +1,55 @@
1
+ import { recoverTypedDataAddress } from "viem";
2
+ import { ballotTypedData, EIP712_SIGNATURE_TYPE } from "../signer/eip712.js";
3
+ /**
4
+ * Offline verify stage 1, signature half: recover the EIP-712 ballot signer and check it
5
+ * equals `bundle.address`. Pure and cheap (no chain read), so it runs first in the gate —
6
+ * a bad signature drops a vote before any fetch or chain work (see DESIGN.md "Transport").
7
+ *
8
+ * The recovered signer *is* the voter (there is no pkc-js author, no author->wallet
9
+ * binding). Rebuilding the typed data from the criteria CID bytes + chainId + the bundle's
10
+ * `votes`/`blockNumber` means a signature gathered for one contest, chain, or block cannot
11
+ * validate on another — the binding is in the hash, not in a trusted `address` field, so a
12
+ * forged `address` simply fails the recovery check here.
13
+ */
14
+ /** True when `s` is `0x`-hex, narrowing it to viem's `Hex` without an `any` cast. */
15
+ function isHex(s) {
16
+ return s.startsWith("0x");
17
+ }
18
+ export async function verifyBundleSignature(args) {
19
+ const { bundle, criteriaCid, chainId } = args;
20
+ if (bundle.signature.type !== EIP712_SIGNATURE_TYPE) {
21
+ return {
22
+ valid: false,
23
+ disposition: "reject",
24
+ reason: `unsupported signature type "${bundle.signature.type}" (expected "${EIP712_SIGNATURE_TYPE}")`
25
+ };
26
+ }
27
+ if (!isHex(bundle.signature.signature)) {
28
+ return { valid: false, disposition: "reject", reason: "signature is not 0x-hex" };
29
+ }
30
+ const typedData = ballotTypedData({
31
+ criteriaCid,
32
+ chainId,
33
+ votes: bundle.votes,
34
+ blockNumber: bundle.blockNumber
35
+ });
36
+ let recovered;
37
+ try {
38
+ recovered = await recoverTypedDataAddress({ ...typedData, signature: bundle.signature.signature });
39
+ }
40
+ catch (err) {
41
+ return { valid: false, disposition: "reject", reason: `signature does not recover: ${err.message}` };
42
+ }
43
+ // The recovered address is EIP-55 checksummed; `bundle.address` may be lowercase or
44
+ // differently cased. Compare case-insensitively.
45
+ if (recovered.toLowerCase() !== bundle.address.toLowerCase()) {
46
+ return {
47
+ valid: false,
48
+ disposition: "reject",
49
+ reason: `recovered signer ${recovered} does not match bundle.address ${bundle.address}`
50
+ };
51
+ }
52
+ return { valid: true };
53
+ }
54
+ /** The {@link OfflineBundleVerifier} seam, backed by {@link verifyBundleSignature}. */
55
+ export const offlineBundleVerifier = { verifyBundleSignature };
@@ -0,0 +1,101 @@
1
+ import type { VotesBundle } from "../schema/votes.js";
2
+ /**
3
+ * Verification interfaces, design only.
4
+ *
5
+ * Verification has one cheap, offline stage (no chain reads) and one chain stage:
6
+ * 1. offline: recover the EIP-712 signer and check it equals bundle.address, and the
7
+ * criteria-bound constraints (votes.length <= maxVotesPerAddress, vote in range).
8
+ * Pairwise-distinct community.publicKeys is enforced even earlier, by
9
+ * VotesBundleSchema at parse time (see DESIGN.md "Votes wire")
10
+ * 2. chain: the `rule` (gate) + weight rules read state at the bucket block, and
11
+ * each vote's community.name claim is resolved through the injected nameResolvers
12
+ * (a name that does not resolve to the claimed publicKey drops the bundle)
13
+ *
14
+ * The tally runs stage 2 lazily (only where it can change the visible ranking), so the
15
+ * cheap offline check is split out and runs first — a bad signature drops a vote for zero
16
+ * chain reads. There is no separate wallet-binding stage: the single wallet signature is
17
+ * the identity. See DESIGN.md "Identity: the voting wallet, nothing else" and "Tally".
18
+ */
19
+ /**
20
+ * How a failing verdict should be blamed, mirroring the gossip gate's `reject`/`ignore`
21
+ * split (see DESIGN.md "Transport"):
22
+ * - "reject": PROVABLY invalid — a pure function of the bundle bytes and pinned historical
23
+ * chain state (bad signature, out-of-range vote, wallet the gate rejects at the bucket
24
+ * block). Deterministic and stable, so it is safe to penalize the sender AND to cache.
25
+ * - "ignore": not provably the sender's fault because the check is view- or clock-dependent
26
+ * and two honest peers can legitimately disagree right now (a `community.name` resolved *at
27
+ * head* during a re-point window; a `blockNumber` bucket ahead of this verifier's chain
28
+ * head). No penalty, and NOT cached — the verdict can change as heads/records converge.
29
+ */
30
+ export type VerdictDisposition = "reject" | "ignore";
31
+ export type VerifyOk = {
32
+ valid: true;
33
+ };
34
+ export type VerifyFail = {
35
+ valid: false;
36
+ disposition: VerdictDisposition;
37
+ reason: string;
38
+ };
39
+ export type VerifyResult = VerifyOk | VerifyFail;
40
+ /** Stage 1: ballot signature only. No chain access. */
41
+ export interface OfflineBundleVerifier {
42
+ /**
43
+ * Rebuild the EIP-712 ballot typed data from the criteria CID bytes + chainId + the
44
+ * bundle's `votes` and `blockNumber` (see signer/eip712.ts), recover the signer with
45
+ * `viem.recoverTypedDataAddress`, and check it equals `bundle.address`. No chain read.
46
+ */
47
+ verifyBundleSignature(args: {
48
+ bundle: VotesBundle;
49
+ criteriaCid: Uint8Array;
50
+ chainId: number;
51
+ }): Promise<VerifyResult>;
52
+ }
53
+ /**
54
+ * A passing full-bundle verdict. Beyond `valid: true` it carries the work the gate already
55
+ * did so downstream stages need not redo it:
56
+ * - `ruleScore`: the gate `rule`'s score for the voting wallet at the bucket block
57
+ * (always `> 0n` here — `0n` would have failed the gate).
58
+ * - `resolvedNames`: for each vote that carried a `community.name`, the `publicKey` the name
59
+ * resolved to (equal to the claimed key, since a mismatch fails the gate). Votes with no
60
+ * name are absent. Lets a UI show a verified name without re-resolving.
61
+ */
62
+ export interface BundleVerdictValid {
63
+ valid: true;
64
+ ruleScore: bigint;
65
+ resolvedNames: Record<string, string>;
66
+ }
67
+ /**
68
+ * The full validity verdict for one bundle: signature + criteria constraints + on-chain
69
+ * gate (`rule`) + community-name resolution, in cheap-to-expensive order with early exit. This is
70
+ * what the gossip forward-gate runs *before* re-forwarding (see DESIGN.md "Transport"): a
71
+ * failing verdict is dropped and never forwarded, stored, or counted. Weight *magnitude*
72
+ * (ranking, not validity) is deliberately NOT computed here — the tally derives it lazily.
73
+ */
74
+ export type BundleVerdict = BundleVerdictValid | VerifyFail;
75
+ /** Runs the full validity pipeline for one already-fetched, schema-parsed bundle. */
76
+ export interface BundleVerifier {
77
+ verify(bundle: VotesBundle): Promise<BundleVerdict>;
78
+ /**
79
+ * Stage 1 only: signature + criteria constraints — local, µs, no chain read and no name
80
+ * resolution. This is what admits a bundle *provisionally* on the cold-join chase path; the
81
+ * deferred network checks (gate chain read, name resolution) then run in the background
82
+ * chain verifier and either confirm the bundle or evict it (see verify/background.ts and
83
+ * DESIGN.md "Background chain verification").
84
+ */
85
+ verifyOffline(bundle: VotesBundle): Promise<VerifyResult>;
86
+ }
87
+ /**
88
+ * The per-bundle record of the two deferred *network* checks. The offline checks (signature,
89
+ * constraints) are never recorded here — they are synchronous preconditions for admission, so
90
+ * an admitted bundle has always passed them.
91
+ *
92
+ * - `chainVerified`: the gate `rule` scored the wallet `> 0n` at the bucket block. `false`
93
+ * means "not yet read", never "failed" — a failed gate evicts the bundle instead.
94
+ * - `nameResolved`: `undefined` when the bundle carries no `community.name`; `false` while
95
+ * the carried name is unresolved; `true` once it resolved to the claimed `publicKey`. A
96
+ * name that resolves to a DIFFERENT key evicts the bundle (it is never counted unchecked).
97
+ */
98
+ export interface BundleChecks {
99
+ chainVerified: boolean;
100
+ nameResolved?: boolean;
101
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@bitsocial/pubsub-voting",
3
+ "version": "0.0.6",
4
+ "description": "Trustless pubsub voting over a shared libp2p/Helia node.",
5
+ "type": "module",
6
+ "license": "GPL-3.0-or-later",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/bitsocialnet/pubsub-voting.git"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "browser": {
21
+ "./dist/storage/node.js": "./dist/storage/browser.js",
22
+ "./dist/transport/announce/node.js": "./dist/transport/announce/browser.js"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "typecheck:examples": "tsc -p tsconfig.examples.json --noEmit",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:integration": "vitest run -c vitest.integration.config.ts",
31
+ "build:bench": "tsc -p benchmark/tsconfig.json",
32
+ "bench:cold-join": "node benchmark/run.mjs",
33
+ "bench:directory-load": "node benchmark/run-directory.mjs",
34
+ "release": "HUSKY=0 release-it --config config/.release-it.json",
35
+ "prepare": "husky",
36
+ "prepublishOnly": "npm run build",
37
+ "commit": "cz"
38
+ },
39
+ "config": {
40
+ "commitizen": {
41
+ "path": "cz-conventional-changelog"
42
+ }
43
+ },
44
+ "dependencies": {
45
+ "@ipld/dag-cbor": "9.2.7",
46
+ "@libp2p/interface": "3.2.4",
47
+ "better-sqlite3": "12.9.0",
48
+ "helia": "6.1.4",
49
+ "localforage": "1.10.0",
50
+ "multiformats": "13.4.2",
51
+ "p-limit": "6.2.0",
52
+ "viem": "2.54.0",
53
+ "zod": "4.3.6"
54
+ },
55
+ "devDependencies": {
56
+ "@chainsafe/libp2p-noise": "17.0.0",
57
+ "@chainsafe/libp2p-yamux": "8.0.1",
58
+ "@commitlint/cli": "20.4.3",
59
+ "@commitlint/config-conventional": "20.4.3",
60
+ "@helia/delegated-routing-v1-http-api-client": "6.0.1",
61
+ "@libp2p/fetch": "4.1.7",
62
+ "@libp2p/gossipsub": "16.0.3",
63
+ "@libp2p/identify": "4.1.8",
64
+ "@libp2p/tcp": "11.0.22",
65
+ "@multiformats/multiaddr": "13.0.3",
66
+ "@pkcprotocol/pkc-js": "0.0.63",
67
+ "@release-it/conventional-changelog": "10.0.6",
68
+ "@types/better-sqlite3": "7.6.13",
69
+ "commitizen": "4.3.1",
70
+ "cz-conventional-changelog": "3.3.0",
71
+ "husky": "9.1.7",
72
+ "libp2p": "3.3.4",
73
+ "release-it": "19.2.4",
74
+ "strip-json-comments": "5.0.3",
75
+ "typescript": "5.9.3",
76
+ "vitest": "4.1.9"
77
+ }
78
+ }