social-kit / README.md
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# ardegazu-social-kit

The social layer of the [ardegazu.ro](https://ardegazu.ro) suite: friends,
presence, invites and co-signed leaderboard receipts — all p2p, riding the
suite's existing relay + store-and-forward mailbox. No servers, and nothing
here weakens the suite's sealing.

## How it works

- **Envelopes** (`envelope.cljs`) — every social message (friend request/accept,
  invite, device sync) is one sign-then-wrap unit sealed to the recipient's
  suite X25519 key (id-kit), context-bound to the recipient identity, deduped
  by id across every transport.
- **Identity inbox** (`pair.cljs`/`mailbox.cljs`) — a mailbox room derived from a
  public idPub: anyone can deposit a sealed envelope, only the key holder can
  read. Honest cost: anyone can also watch envelope count/timing, and flood it.
- **Pair channels** (`pair.cljs`) — per friendship, both sides derive the same
  gossip topic + mailbox drop from a static-static DH secret. Topics are
  HKDF-opaque, senders are keyed tags: outsiders can't link a topic to anyone.
- **The social agent** (`presence.cljs`) — one per app tab: sealed presence
  beacons (25 s, visibility-gated), live envelope delivery, offline catch-up.
  Takes the app's own libp2p pubsub (`PubsubLike` works on both generations
  the suite runs); apps without a node use `ardegazu-social-kit/net`. A peer
  may also advertise the brain/model versions it is playing (`brains`, since
  2.1.0) — clamped on both sides, since the beacon re-read is the only
  update mechanism.
- **Friends** (`friends.cljs`) — the store + tombstoned cross-device fold +
  capability friend links (`https://ardegazu.ro/#add=<pub>.<x>.<xSig>`).
- **Receipts** (`receipts.cljs`) — match results co-signed by the players
  present (≥2 distinct signers or it doesn't count), dropped into a public
  per-game weekly mailbox room whose TTL IS the rolling window, carried +
  gossip-merged for all-time. A scoreboard among friends, not anti-cheat.

Since v2.0.0 the implementation is ClojureScript
(`src/ardegazu/social/*.cljs`, per the suite's CLJS canon in
`dev/docs/CLJS.md`). Consumers see compiled, deterministic ESM in `dist/`
plus hand-authored `types/*.d.ts` — the wire and storage contracts are
unchanged and locked by golden vectors extracted from the v1.2.0 TypeScript
implementation (`test/vectors/`, byte-exact signatures and TS-sealed blobs
included).

## Consuming

```jsonc
"dependencies": { "ardegazu-social-kit": "git+https://git.ardegazu.ro/social-kit.git#v2.1.0" }
```

The package ships compiled ESM (`dist/`) with TypeScript declarations
(`types/`) — no transpile-the-source config needed (v1 consumers can drop
their `optimizeDeps.exclude` / `fs.allow` entries whenever convenient).
Subpath exports are real split points: `.` (the whole social layer, no
libp2p), `./net` (the hub's standalone node — the only entry referencing
libp2p), `./join` (the dependency-free paste/home chips), `./selftest`.

Peer deps: `ardegazu-id-kit` + `@noble/curves` always; the libp2p v3 family
only if you import `ardegazu-social-kit/net` (the hub does; apps with their
own node don't).

## App chrome & theming

The kit's fixed chrome (chips, invite sheet, toasts, the paste chip) is
safe-area aware and themeable. Every color reads a `--soc-*` custom property
with the historical value as the fallback, so apps that do nothing render
exactly as before. Opting in is five lines on `:root`:

```css
:root {
  --soc-bg: #101418; --soc-fg: #e8eef2; --soc-dim: #8fa0ad;
  --soc-line: #24303a; --soc-accent: #7fd0a8;
}
```

Two body classes give apps control over when chrome shows: `soc-play` on
`<body>` hides the kit's chips (gameplay stays clean), `soc-menu` shows the
`homeChip` — a small fixed "back to the hub" link (`homeChip()` from
`ardegazu-social-kit/join`, idempotent, renders nothing until `soc-menu`).

The invite sheet is share-first: a full-width "share the room link" row
(`navigator.share`, clipboard where share doesn't exist) above the friend
list, and with no friends saved it says so honestly and points at the hub's
friend flow. The invite chip therefore renders even friendless; the
"friends can join" toggle still needs friends to grant to.

## Multi-device sync

The self channel's `fsync` envelope carries, next to the friend-list `soc`
blob, three optional sections old kits ignore (`selfsync.cljs`):

- `profile` — a `ProfileSyncBlob` (`name/hue/glyph/lang/ts`), folded with
  `foldProfileSync`: strictly newer wins wholesale, otherwise remote only
  backfills locally-blank fields; a blank blob never clobbers a real profile.
- `app` — one app-scoped state blob, `ns`-gated on receive.
- `apps` — the suite identity record's per-app map, `AppsSyncMap`
  (`{<appKey>: {state, ts}}`), folded with `foldAppsSync` **per key**: every
  app writes its own key and reads all of them, so a chat edit on the phone
  and a board edit on the laptop are independent facts and neither erases the
  other. Strictly newer `ts` wins that key wholesale; equal `ts` breaks on the
  greater canonical serialization, computed role-independently so both devices
  converge in one exchange. `sanitizeAppsSync` clamps the shape: keys
  `/^[a-z0-9-]{1,32}$/`, entries `{state, ts}` with `ts` clamped to **now**
  (never ahead of it), and the two size caps copied from id-kit — 16384 UTF-16
  code units of `JSON.stringify(state)` per entry, 98304 for the whole
  serialized map. **The unit is code units, not UTF-8 bytes**, because id-kit
  owns the record and measures it that way; measuring bytes here would silently
  drop ro/hu sections that id-kit had already stored. No entry-count cap and no
  eviction, because it is the user's own data. Key order is *deterministic*,
  not lexicographic — the charset permits all-digit keys and JS canonical
  property order hoists those numerically (`"2"`, `"10"`, then `"board"`).

All three are applied only when the envelope came from your own key (the
self-origin guard), each inside its own try, so a misbehaving hook can never
starve the next section or the friend-list fold.

Apps opt in by passing `profileSync: {get, apply}`, `appState: {get, apply}`
and/or `appsSync: {get, apply}` through `attachSocial` (or
`SocialAgentOpts`), and call
`agent.syncNow()` after local edits — a debounced (~1.5 s), hash-gated
self-sync: unchanged payloads skip the wire unless the last mailbox deposit
is >20 h old (the deposit refreshes the self drop's 7-day TTL).

## Privacy & abuse, honestly

Presence is visible to accepted friends only and exists only while a tab is
open. Inbox ids are derivable from public friend links (flood/watch metadata
possible; contents stay sealed). Unfriending doesn't un-know: an ex-friend
keeps the pair secret forever — blocking stops processing, not derivability.

## Releasing

```sh
npm run check                       # text.d.ts sync + shadow-cljs compile + consumer.ts type smoke
npm run build && npm test           # rebuild dist/, golden vectors + selftest against it
deploy/check-dist.sh                # committed dist == fresh build, no local paths
git commit … && git tag vX.Y.Z
cd ../git && ./assemble.sh social-kit
```

`dist/` is committed and must be byte-deterministic: exact-pinned
clojurescript + shadow-cljs in `deps.edn`, `scripts/normalize-gensyms.mjs` in
the build, and `deploy/check-dist.sh` as the gate (see `dev/docs/CLJS.md`).
`types/text.d.ts` (the `SocialTextKey` union) is generated from the EN table
by `scripts/gen-text-dts.mjs`; `npm run check` fails on drift.

## Changelog

- **2.1.0** — optional `brains` on the agent/`attachSocial`: a thunk read at
  every beacon, announcing the brain/model versions this peer is playing as
  one additive `br` key (game id → version), surfaced as `brains` on the
  presence entry. Bots fill it; browser clients leave it unset and their
  beacon bytes are unchanged, as are the entries older peers produce.
  Untrusted on arrival, so the same clamp runs on send and receive: plain
  object only, ≤8 entries, keys `^[a-z0-9][a-z0-9-]{0,23}$`, values strings
  matching `^[A-Za-z0-9@._-]{1,32}$` — render them via `textContent`, never
  `innerHTML`.

- **2.0.0** — ClojureScript port, whole-kit. Same API, same wire: the
  committed golden-vector suite (canon quirks, envelope signatures, sealed
  wrapbox/pair fixtures, byte-exact receipt co-signatures, isoweek edges,
  friend links/folds, the 28 `SocialTextKey`s) runs against the compiled
  `dist/` and stays green. Consumers switch from compiling the TS source to
  compiled ESM + shipped `.d.ts`.

- **1.2.0** — SECURITY: `fsync` envelopes are now applied only when sent by
  the receiving identity itself. Identity-inbox room ids are publicly
  derivable, so before this fix a stranger could seal a valid fsync (signed
  with their own key) into someone's inbox and inject friend records via the
  merge. Kits older than 1.2.0 stay exposed to this friend-injection until
  their apps update. Also: optional profile/app sections on the self fsync +
  `syncNow()`, `homeChip`, `--soc-*` theming, safe-area/mobile chrome.

MIT.

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/social-kit.git