@weaveprotocol/core 0.1.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.
Files changed (430) hide show
  1. package/README.md +1106 -0
  2. package/dist/elements/auth-styles.d.ts +18 -0
  3. package/dist/elements/auth-styles.d.ts.map +1 -0
  4. package/dist/elements/auth-styles.js +150 -0
  5. package/dist/elements/auth-styles.js.map +1 -0
  6. package/dist/elements/dom.d.ts +28 -0
  7. package/dist/elements/dom.d.ts.map +1 -0
  8. package/dist/elements/dom.js +74 -0
  9. package/dist/elements/dom.js.map +1 -0
  10. package/dist/elements/index.d.ts +13 -0
  11. package/dist/elements/index.d.ts.map +1 -0
  12. package/dist/elements/index.js +11 -0
  13. package/dist/elements/index.js.map +1 -0
  14. package/dist/elements/weave-auth.d.ts +52 -0
  15. package/dist/elements/weave-auth.d.ts.map +1 -0
  16. package/dist/elements/weave-auth.js +427 -0
  17. package/dist/elements/weave-auth.js.map +1 -0
  18. package/dist/identity/account-store.d.ts +113 -0
  19. package/dist/identity/account-store.d.ts.map +1 -0
  20. package/dist/identity/account-store.js +305 -0
  21. package/dist/identity/account-store.js.map +1 -0
  22. package/dist/identity/account-vault.d.ts +199 -0
  23. package/dist/identity/account-vault.d.ts.map +1 -0
  24. package/dist/identity/account-vault.js +251 -0
  25. package/dist/identity/account-vault.js.map +1 -0
  26. package/dist/identity/agent-note.d.ts +19 -0
  27. package/dist/identity/agent-note.d.ts.map +1 -0
  28. package/dist/identity/agent-note.js +29 -0
  29. package/dist/identity/agent-note.js.map +1 -0
  30. package/dist/identity/crypto-p256.d.ts +7 -0
  31. package/dist/identity/crypto-p256.d.ts.map +1 -0
  32. package/dist/identity/crypto-p256.js +84 -0
  33. package/dist/identity/crypto-p256.js.map +1 -0
  34. package/dist/identity/device-key.d.ts +60 -0
  35. package/dist/identity/device-key.d.ts.map +1 -0
  36. package/dist/identity/device-key.js +103 -0
  37. package/dist/identity/device-key.js.map +1 -0
  38. package/dist/identity/did.d.ts +22 -0
  39. package/dist/identity/did.d.ts.map +1 -0
  40. package/dist/identity/did.js +37 -0
  41. package/dist/identity/did.js.map +1 -0
  42. package/dist/identity/folder-account.d.ts +65 -0
  43. package/dist/identity/folder-account.d.ts.map +1 -0
  44. package/dist/identity/folder-account.js +115 -0
  45. package/dist/identity/folder-account.js.map +1 -0
  46. package/dist/identity/identity-manager.d.ts +36 -0
  47. package/dist/identity/identity-manager.d.ts.map +1 -0
  48. package/dist/identity/identity-manager.js +115 -0
  49. package/dist/identity/identity-manager.js.map +1 -0
  50. package/dist/identity/index.d.ts +10 -0
  51. package/dist/identity/index.d.ts.map +1 -0
  52. package/dist/identity/index.js +10 -0
  53. package/dist/identity/index.js.map +1 -0
  54. package/dist/identity/keys.d.ts +24 -0
  55. package/dist/identity/keys.d.ts.map +1 -0
  56. package/dist/identity/keys.js +38 -0
  57. package/dist/identity/keys.js.map +1 -0
  58. package/dist/identity/pairing.d.ts +83 -0
  59. package/dist/identity/pairing.d.ts.map +1 -0
  60. package/dist/identity/pairing.js +120 -0
  61. package/dist/identity/pairing.js.map +1 -0
  62. package/dist/identity/passkey-diagnostics.d.ts +54 -0
  63. package/dist/identity/passkey-diagnostics.d.ts.map +1 -0
  64. package/dist/identity/passkey-diagnostics.js +178 -0
  65. package/dist/identity/passkey-diagnostics.js.map +1 -0
  66. package/dist/identity/recovery-code.d.ts +50 -0
  67. package/dist/identity/recovery-code.d.ts.map +1 -0
  68. package/dist/identity/recovery-code.js +116 -0
  69. package/dist/identity/recovery-code.js.map +1 -0
  70. package/dist/identity/root-signer.d.ts +59 -0
  71. package/dist/identity/root-signer.d.ts.map +1 -0
  72. package/dist/identity/root-signer.js +44 -0
  73. package/dist/identity/root-signer.js.map +1 -0
  74. package/dist/identity/ucan.d.ts +164 -0
  75. package/dist/identity/ucan.d.ts.map +1 -0
  76. package/dist/identity/ucan.js +273 -0
  77. package/dist/identity/ucan.js.map +1 -0
  78. package/dist/identity/webauthn.d.ts +87 -0
  79. package/dist/identity/webauthn.d.ts.map +1 -0
  80. package/dist/identity/webauthn.js +155 -0
  81. package/dist/identity/webauthn.js.map +1 -0
  82. package/dist/index.d.ts +116 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +85 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/network/index.d.ts +17 -0
  87. package/dist/network/index.d.ts.map +1 -0
  88. package/dist/network/index.js +10 -0
  89. package/dist/network/index.js.map +1 -0
  90. package/dist/network/introductions.d.ts +85 -0
  91. package/dist/network/introductions.d.ts.map +1 -0
  92. package/dist/network/introductions.js +94 -0
  93. package/dist/network/introductions.js.map +1 -0
  94. package/dist/network/local-transport.d.ts +22 -0
  95. package/dist/network/local-transport.d.ts.map +1 -0
  96. package/dist/network/local-transport.js +55 -0
  97. package/dist/network/local-transport.js.map +1 -0
  98. package/dist/network/mesh.d.ts +31 -0
  99. package/dist/network/mesh.d.ts.map +1 -0
  100. package/dist/network/mesh.js +332 -0
  101. package/dist/network/mesh.js.map +1 -0
  102. package/dist/network/multi-signaling.d.ts +29 -0
  103. package/dist/network/multi-signaling.d.ts.map +1 -0
  104. package/dist/network/multi-signaling.js +125 -0
  105. package/dist/network/multi-signaling.js.map +1 -0
  106. package/dist/network/network-manager.d.ts +37 -0
  107. package/dist/network/network-manager.d.ts.map +1 -0
  108. package/dist/network/network-manager.js +64 -0
  109. package/dist/network/network-manager.js.map +1 -0
  110. package/dist/network/peer-auth.d.ts +98 -0
  111. package/dist/network/peer-auth.d.ts.map +1 -0
  112. package/dist/network/peer-auth.js +86 -0
  113. package/dist/network/peer-auth.js.map +1 -0
  114. package/dist/network/rtc-transport.d.ts +18 -0
  115. package/dist/network/rtc-transport.d.ts.map +1 -0
  116. package/dist/network/rtc-transport.js +149 -0
  117. package/dist/network/rtc-transport.js.map +1 -0
  118. package/dist/network/signaling.d.ts +40 -0
  119. package/dist/network/signaling.d.ts.map +1 -0
  120. package/dist/network/signaling.js +124 -0
  121. package/dist/network/signaling.js.map +1 -0
  122. package/dist/network/transport.d.ts +50 -0
  123. package/dist/network/transport.d.ts.map +1 -0
  124. package/dist/network/transport.js +13 -0
  125. package/dist/network/transport.js.map +1 -0
  126. package/dist/network/ws-transport.d.ts +42 -0
  127. package/dist/network/ws-transport.d.ts.map +1 -0
  128. package/dist/network/ws-transport.js +194 -0
  129. package/dist/network/ws-transport.js.map +1 -0
  130. package/dist/node/actions.d.ts +62 -0
  131. package/dist/node/actions.d.ts.map +1 -0
  132. package/dist/node/actions.js +500 -0
  133. package/dist/node/actions.js.map +1 -0
  134. package/dist/node/carrier.d.ts +85 -0
  135. package/dist/node/carrier.d.ts.map +1 -0
  136. package/dist/node/carrier.js +192 -0
  137. package/dist/node/carrier.js.map +1 -0
  138. package/dist/node/copy.d.ts +38 -0
  139. package/dist/node/copy.d.ts.map +1 -0
  140. package/dist/node/copy.js +53 -0
  141. package/dist/node/copy.js.map +1 -0
  142. package/dist/node/index.d.ts +16 -0
  143. package/dist/node/index.d.ts.map +1 -0
  144. package/dist/node/index.js +11 -0
  145. package/dist/node/index.js.map +1 -0
  146. package/dist/node/node.d.ts +12 -0
  147. package/dist/node/node.d.ts.map +1 -0
  148. package/dist/node/node.js +872 -0
  149. package/dist/node/node.js.map +1 -0
  150. package/dist/node/space-runtime.d.ts +144 -0
  151. package/dist/node/space-runtime.d.ts.map +1 -0
  152. package/dist/node/space-runtime.js +1221 -0
  153. package/dist/node/space-runtime.js.map +1 -0
  154. package/dist/node/stores.d.ts +45 -0
  155. package/dist/node/stores.d.ts.map +1 -0
  156. package/dist/node/stores.js +31 -0
  157. package/dist/node/stores.js.map +1 -0
  158. package/dist/node/types.d.ts +508 -0
  159. package/dist/node/types.d.ts.map +1 -0
  160. package/dist/node/types.js +2 -0
  161. package/dist/node/types.js.map +1 -0
  162. package/dist/privacy/index.d.ts +4 -0
  163. package/dist/privacy/index.d.ts.map +1 -0
  164. package/dist/privacy/index.js +4 -0
  165. package/dist/privacy/index.js.map +1 -0
  166. package/dist/privacy/key-distribution.d.ts +42 -0
  167. package/dist/privacy/key-distribution.d.ts.map +1 -0
  168. package/dist/privacy/key-distribution.js +50 -0
  169. package/dist/privacy/key-distribution.js.map +1 -0
  170. package/dist/privacy/privacy-guard.d.ts +24 -0
  171. package/dist/privacy/privacy-guard.d.ts.map +1 -0
  172. package/dist/privacy/privacy-guard.js +87 -0
  173. package/dist/privacy/privacy-guard.js.map +1 -0
  174. package/dist/privacy/space-encryption.d.ts +48 -0
  175. package/dist/privacy/space-encryption.d.ts.map +1 -0
  176. package/dist/privacy/space-encryption.js +61 -0
  177. package/dist/privacy/space-encryption.js.map +1 -0
  178. package/dist/query/engine.d.ts +27 -0
  179. package/dist/query/engine.d.ts.map +1 -0
  180. package/dist/query/engine.js +84 -0
  181. package/dist/query/engine.js.map +1 -0
  182. package/dist/query/filter.d.ts +15 -0
  183. package/dist/query/filter.d.ts.map +1 -0
  184. package/dist/query/filter.js +207 -0
  185. package/dist/query/filter.js.map +1 -0
  186. package/dist/query/types.d.ts +144 -0
  187. package/dist/query/types.d.ts.map +1 -0
  188. package/dist/query/types.js +22 -0
  189. package/dist/query/types.js.map +1 -0
  190. package/dist/react/context.d.ts +81 -0
  191. package/dist/react/context.d.ts.map +1 -0
  192. package/dist/react/context.js +95 -0
  193. package/dist/react/context.js.map +1 -0
  194. package/dist/react/index.d.ts +40 -0
  195. package/dist/react/index.d.ts.map +1 -0
  196. package/dist/react/index.js +36 -0
  197. package/dist/react/index.js.map +1 -0
  198. package/dist/react/use-live.d.ts +16 -0
  199. package/dist/react/use-live.d.ts.map +1 -0
  200. package/dist/react/use-live.js +55 -0
  201. package/dist/react/use-live.js.map +1 -0
  202. package/dist/react/use-query.d.ts +17 -0
  203. package/dist/react/use-query.d.ts.map +1 -0
  204. package/dist/react/use-query.js +23 -0
  205. package/dist/react/use-query.js.map +1 -0
  206. package/dist/react/use-space.d.ts +29 -0
  207. package/dist/react/use-space.d.ts.map +1 -0
  208. package/dist/react/use-space.js +48 -0
  209. package/dist/react/use-space.js.map +1 -0
  210. package/dist/react/use-spaces.d.ts +19 -0
  211. package/dist/react/use-spaces.d.ts.map +1 -0
  212. package/dist/react/use-spaces.js +55 -0
  213. package/dist/react/use-spaces.js.map +1 -0
  214. package/dist/react/use-weave-auth.d.ts +10 -0
  215. package/dist/react/use-weave-auth.d.ts.map +1 -0
  216. package/dist/react/use-weave-auth.js +15 -0
  217. package/dist/react/use-weave-auth.js.map +1 -0
  218. package/dist/react/weave-auth.d.ts +17 -0
  219. package/dist/react/weave-auth.d.ts.map +1 -0
  220. package/dist/react/weave-auth.js +38 -0
  221. package/dist/react/weave-auth.js.map +1 -0
  222. package/dist/records/describe.d.ts +38 -0
  223. package/dist/records/describe.d.ts.map +1 -0
  224. package/dist/records/describe.js +101 -0
  225. package/dist/records/describe.js.map +1 -0
  226. package/dist/records/links.d.ts +14 -0
  227. package/dist/records/links.d.ts.map +1 -0
  228. package/dist/records/links.js +25 -0
  229. package/dist/records/links.js.map +1 -0
  230. package/dist/records/rules.d.ts +59 -0
  231. package/dist/records/rules.d.ts.map +1 -0
  232. package/dist/records/rules.js +118 -0
  233. package/dist/records/rules.js.map +1 -0
  234. package/dist/records/version.d.ts +54 -0
  235. package/dist/records/version.d.ts.map +1 -0
  236. package/dist/records/version.js +70 -0
  237. package/dist/records/version.js.map +1 -0
  238. package/dist/schema/collection-def.d.ts +96 -0
  239. package/dist/schema/collection-def.d.ts.map +1 -0
  240. package/dist/schema/collection-def.js +272 -0
  241. package/dist/schema/collection-def.js.map +1 -0
  242. package/dist/schema/expression.d.ts +69 -0
  243. package/dist/schema/expression.d.ts.map +1 -0
  244. package/dist/schema/expression.js +92 -0
  245. package/dist/schema/expression.js.map +1 -0
  246. package/dist/schema/index.d.ts +4 -0
  247. package/dist/schema/index.d.ts.map +1 -0
  248. package/dist/schema/index.js +4 -0
  249. package/dist/schema/index.js.map +1 -0
  250. package/dist/schema/schema-engine.d.ts +42 -0
  251. package/dist/schema/schema-engine.d.ts.map +1 -0
  252. package/dist/schema/schema-engine.js +36 -0
  253. package/dist/schema/schema-engine.js.map +1 -0
  254. package/dist/schema/signer.d.ts +27 -0
  255. package/dist/schema/signer.d.ts.map +1 -0
  256. package/dist/schema/signer.js +36 -0
  257. package/dist/schema/signer.js.map +1 -0
  258. package/dist/schemas/apps.d.ts +88 -0
  259. package/dist/schemas/apps.d.ts.map +1 -0
  260. package/dist/schemas/apps.js +168 -0
  261. package/dist/schemas/apps.js.map +1 -0
  262. package/dist/schemas/index.d.ts +410 -0
  263. package/dist/schemas/index.d.ts.map +1 -0
  264. package/dist/schemas/index.js +208 -0
  265. package/dist/schemas/index.js.map +1 -0
  266. package/dist/schemas/screens.d.ts +67 -0
  267. package/dist/schemas/screens.d.ts.map +1 -0
  268. package/dist/schemas/screens.js +242 -0
  269. package/dist/schemas/screens.js.map +1 -0
  270. package/dist/session/agent-link.d.ts +79 -0
  271. package/dist/session/agent-link.d.ts.map +1 -0
  272. package/dist/session/agent-link.js +251 -0
  273. package/dist/session/agent-link.js.map +1 -0
  274. package/dist/session/auth.d.ts +233 -0
  275. package/dist/session/auth.d.ts.map +1 -0
  276. package/dist/session/auth.js +783 -0
  277. package/dist/session/auth.js.map +1 -0
  278. package/dist/session/connect.d.ts +211 -0
  279. package/dist/session/connect.d.ts.map +1 -0
  280. package/dist/session/connect.js +355 -0
  281. package/dist/session/connect.js.map +1 -0
  282. package/dist/session/connection.d.ts +70 -0
  283. package/dist/session/connection.d.ts.map +1 -0
  284. package/dist/session/connection.js +120 -0
  285. package/dist/session/connection.js.map +1 -0
  286. package/dist/session/credentials.d.ts +44 -0
  287. package/dist/session/credentials.d.ts.map +1 -0
  288. package/dist/session/credentials.js +60 -0
  289. package/dist/session/credentials.js.map +1 -0
  290. package/dist/session/index.d.ts +25 -0
  291. package/dist/session/index.d.ts.map +1 -0
  292. package/dist/session/index.js +18 -0
  293. package/dist/session/index.js.map +1 -0
  294. package/dist/session/pairing.d.ts +59 -0
  295. package/dist/session/pairing.d.ts.map +1 -0
  296. package/dist/session/pairing.js +144 -0
  297. package/dist/session/pairing.js.map +1 -0
  298. package/dist/session/places.d.ts +62 -0
  299. package/dist/session/places.d.ts.map +1 -0
  300. package/dist/session/places.js +101 -0
  301. package/dist/session/places.js.map +1 -0
  302. package/dist/session/stay-signed-in.d.ts +37 -0
  303. package/dist/session/stay-signed-in.d.ts.map +1 -0
  304. package/dist/session/stay-signed-in.js +124 -0
  305. package/dist/session/stay-signed-in.js.map +1 -0
  306. package/dist/space/account-registry.d.ts +64 -0
  307. package/dist/space/account-registry.d.ts.map +1 -0
  308. package/dist/space/account-registry.js +66 -0
  309. package/dist/space/account-registry.js.map +1 -0
  310. package/dist/space/index.d.ts +3 -0
  311. package/dist/space/index.d.ts.map +1 -0
  312. package/dist/space/index.js +3 -0
  313. package/dist/space/index.js.map +1 -0
  314. package/dist/space/pass.d.ts +63 -0
  315. package/dist/space/pass.d.ts.map +1 -0
  316. package/dist/space/pass.js +54 -0
  317. package/dist/space/pass.js.map +1 -0
  318. package/dist/space/presets.d.ts +31 -0
  319. package/dist/space/presets.d.ts.map +1 -0
  320. package/dist/space/presets.js +24 -0
  321. package/dist/space/presets.js.map +1 -0
  322. package/dist/space/roles.d.ts +186 -0
  323. package/dist/space/roles.d.ts.map +1 -0
  324. package/dist/space/roles.js +500 -0
  325. package/dist/space/roles.js.map +1 -0
  326. package/dist/space/space-access.d.ts +80 -0
  327. package/dist/space/space-access.d.ts.map +1 -0
  328. package/dist/space/space-access.js +135 -0
  329. package/dist/space/space-access.js.map +1 -0
  330. package/dist/space/space-manager.d.ts +87 -0
  331. package/dist/space/space-manager.d.ts.map +1 -0
  332. package/dist/space/space-manager.js +187 -0
  333. package/dist/space/space-manager.js.map +1 -0
  334. package/dist/storage/directory-access.d.ts +78 -0
  335. package/dist/storage/directory-access.d.ts.map +1 -0
  336. package/dist/storage/directory-access.js +164 -0
  337. package/dist/storage/directory-access.js.map +1 -0
  338. package/dist/storage/encrypted-adapter.d.ts +49 -0
  339. package/dist/storage/encrypted-adapter.d.ts.map +1 -0
  340. package/dist/storage/encrypted-adapter.js +106 -0
  341. package/dist/storage/encrypted-adapter.js.map +1 -0
  342. package/dist/storage/folder-adapter.d.ts +105 -0
  343. package/dist/storage/folder-adapter.d.ts.map +1 -0
  344. package/dist/storage/folder-adapter.js +280 -0
  345. package/dist/storage/folder-adapter.js.map +1 -0
  346. package/dist/storage/folder-reconcile.d.ts +53 -0
  347. package/dist/storage/folder-reconcile.d.ts.map +1 -0
  348. package/dist/storage/folder-reconcile.js +65 -0
  349. package/dist/storage/folder-reconcile.js.map +1 -0
  350. package/dist/storage/index.d.ts +10 -0
  351. package/dist/storage/index.d.ts.map +1 -0
  352. package/dist/storage/index.js +9 -0
  353. package/dist/storage/index.js.map +1 -0
  354. package/dist/storage/indexeddb-adapter.d.ts +12 -0
  355. package/dist/storage/indexeddb-adapter.d.ts.map +1 -0
  356. package/dist/storage/indexeddb-adapter.js +190 -0
  357. package/dist/storage/indexeddb-adapter.js.map +1 -0
  358. package/dist/storage/mst.d.ts +121 -0
  359. package/dist/storage/mst.d.ts.map +1 -0
  360. package/dist/storage/mst.js +402 -0
  361. package/dist/storage/mst.js.map +1 -0
  362. package/dist/storage/storage-provider.d.ts +85 -0
  363. package/dist/storage/storage-provider.d.ts.map +1 -0
  364. package/dist/storage/storage-provider.js +220 -0
  365. package/dist/storage/storage-provider.js.map +1 -0
  366. package/dist/sync/anti-entropy.d.ts +49 -0
  367. package/dist/sync/anti-entropy.d.ts.map +1 -0
  368. package/dist/sync/anti-entropy.js +62 -0
  369. package/dist/sync/anti-entropy.js.map +1 -0
  370. package/dist/sync/index.d.ts +4 -0
  371. package/dist/sync/index.d.ts.map +1 -0
  372. package/dist/sync/index.js +4 -0
  373. package/dist/sync/index.js.map +1 -0
  374. package/dist/sync/sync-engine.d.ts +71 -0
  375. package/dist/sync/sync-engine.d.ts.map +1 -0
  376. package/dist/sync/sync-engine.js +309 -0
  377. package/dist/sync/sync-engine.js.map +1 -0
  378. package/dist/sync/sync-messages.d.ts +75 -0
  379. package/dist/sync/sync-messages.d.ts.map +1 -0
  380. package/dist/sync/sync-messages.js +12 -0
  381. package/dist/sync/sync-messages.js.map +1 -0
  382. package/dist/types.d.ts +243 -0
  383. package/dist/types.d.ts.map +1 -0
  384. package/dist/types.js +7 -0
  385. package/dist/types.js.map +1 -0
  386. package/dist/utils/encoding.d.ts +63 -0
  387. package/dist/utils/encoding.d.ts.map +1 -0
  388. package/dist/utils/encoding.js +130 -0
  389. package/dist/utils/encoding.js.map +1 -0
  390. package/dist/utils/errors.d.ts +43 -0
  391. package/dist/utils/errors.d.ts.map +1 -0
  392. package/dist/utils/errors.js +28 -0
  393. package/dist/utils/errors.js.map +1 -0
  394. package/dist/utils/events.d.ts +19 -0
  395. package/dist/utils/events.d.ts.map +1 -0
  396. package/dist/utils/events.js +27 -0
  397. package/dist/utils/events.js.map +1 -0
  398. package/dist/utils/hash.d.ts +23 -0
  399. package/dist/utils/hash.d.ts.map +1 -0
  400. package/dist/utils/hash.js +49 -0
  401. package/dist/utils/hash.js.map +1 -0
  402. package/dist/utils/index.d.ts +9 -0
  403. package/dist/utils/index.d.ts.map +1 -0
  404. package/dist/utils/index.js +9 -0
  405. package/dist/utils/index.js.map +1 -0
  406. package/dist/validation/capability-gate.d.ts +50 -0
  407. package/dist/validation/capability-gate.d.ts.map +1 -0
  408. package/dist/validation/capability-gate.js +71 -0
  409. package/dist/validation/capability-gate.js.map +1 -0
  410. package/dist/validation/crypto-gate.d.ts +16 -0
  411. package/dist/validation/crypto-gate.d.ts.map +1 -0
  412. package/dist/validation/crypto-gate.js +37 -0
  413. package/dist/validation/crypto-gate.js.map +1 -0
  414. package/dist/validation/index.d.ts +6 -0
  415. package/dist/validation/index.d.ts.map +1 -0
  416. package/dist/validation/index.js +6 -0
  417. package/dist/validation/index.js.map +1 -0
  418. package/dist/validation/stateful-gate.d.ts +15 -0
  419. package/dist/validation/stateful-gate.d.ts.map +1 -0
  420. package/dist/validation/stateful-gate.js +50 -0
  421. package/dist/validation/stateful-gate.js.map +1 -0
  422. package/dist/validation/structural-gate.d.ts +18 -0
  423. package/dist/validation/structural-gate.d.ts.map +1 -0
  424. package/dist/validation/structural-gate.js +54 -0
  425. package/dist/validation/structural-gate.js.map +1 -0
  426. package/dist/validation/validation-engine.d.ts +28 -0
  427. package/dist/validation/validation-engine.d.ts.map +1 -0
  428. package/dist/validation/validation-engine.js +38 -0
  429. package/dist/validation/validation-engine.js.map +1 -0
  430. package/package.json +107 -0
package/README.md ADDED
@@ -0,0 +1,1106 @@
1
+ # @weaveprotocol/core
2
+
3
+ A peer-to-peer data protocol for the browser. You own your identity as a
4
+ written-down code, keep your data in signed records that sync directly between
5
+ devices, and every app is a view onto that data rather than its owner.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ ┌──────────────────────────────────────────────────────────────┐
11
+ │ Applications │
12
+ ├──────────────┬───────────┬────────────┬──────────┬───────────┤
13
+ │ Accounts │ Spaces │ Validation │ Privacy │ Sync │
14
+ │ seed, vault, │ roles, │ crypto → │ AES-GCM │ MST anti- │
15
+ │ root signer, │ members × │ structural │ per │ entropy │
16
+ │ UCAN, pairing│ pub/priv │ → UCAN │ space │ gossip │
17
+ ├──────────────┴───────────┴────────────┴──────────┴───────────┤
18
+ │ Storage: Merkle Search Tree over a StorageAdapter │
19
+ │ IndexedDB (per origin) · data folder (shared by origins) │
20
+ ├──────────────────────────────────────────────────────────────┤
21
+ │ Network: WebRTC data channels │
22
+ │ several relays at once · peers introduce peers │
23
+ ├──────────────────────────────────────────────────────────────┤
24
+ │ Web Crypto · WebAuthn · IndexedDB · File System Access · │
25
+ │ WebRTC · @noble/curves · @scure/base │
26
+ └──────────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ ## Key Principles
30
+
31
+ - **No authority.** No server issues identities or holds the truth. Relays only
32
+ introduce peers; an always-on node adds availability, never authority.
33
+ - **Apps are views.** Data lives in spaces the user owns, as signed records any
34
+ app can read and verify.
35
+ - **Local-first.** Works offline, syncs when peers are reachable.
36
+ - **Few, boring dependencies.** Native browser APIs first. Where a problem is
37
+ hard and already solved — elliptic-curve arithmetic, for one — a very stable,
38
+ widely used library instead of our own. See [docs/DEPENDENCIES.md](docs/DEPENDENCIES.md).
39
+ - **Isomorphic.** Runs in browsers, Node and Bun via `globalThis`.
40
+ - **Functional.** Plain functions and frozen data, no class hierarchies.
41
+ - **Standard Schema.** Bring your own validator (Zod, Valibot, ArkType, …).
42
+
43
+ ## Quick Start
44
+
45
+ ```typescript
46
+ import {
47
+ generateSeed, seedToRecoveryCode, createIdentityManager, createLocalRootSigner,
48
+ publicKeyToDid, P256_MULTICODEC, createSigner, createExpression,
49
+ createIndexedDBAdapter, createStorageProvider, createSpaceManager,
50
+ } from '@weaveprotocol/core';
51
+
52
+ // 1. An account is a 16-byte seed. Show the code once; the user keeps it.
53
+ const seed = generateSeed();
54
+ console.log(seedToRecoveryCode(seed)); // 'K7N6-ERYP-68TZ-A7HN-VJW3-QWKN-CG'
55
+
56
+ const manager = createIdentityManager();
57
+ const me = await manager.fromSeed(seed); // same seed → same DID, anywhere
58
+ const provider = manager.getProvider();
59
+
60
+ // 2. The root key signs one thing: permission for a session key to write.
61
+ const root = createLocalRootSigner(me, provider);
62
+ const session = await provider.generateKeyPair();
63
+ const sessionDid = publicKeyToDid(await provider.exportPublicKey(session.publicKey), P256_MULTICODEC);
64
+ const ucan = await root.delegate({
65
+ audience: sessionDid,
66
+ capabilities: [{ with: '*', can: 'expression/*' }],
67
+ expiration: Math.floor(Date.now() / 1000) + 3600,
68
+ });
69
+
70
+ // 3. A space to put things in
71
+ const spaces = createSpaceManager(await createIndexedDBAdapter('my-app/registry'));
72
+ const { space } = await spaces.create({ name: 'Notes', visibility: 'public', creator: me.did });
73
+
74
+ // 4. A signed record, stored in that space's own Merkle tree
75
+ const storage = createStorageProvider(await createIndexedDBAdapter(`my-app/space/${space.id}`));
76
+ const signed = await createSigner(provider).sign(
77
+ createExpression({
78
+ author: sessionDid,
79
+ collection: 'app.example.note',
80
+ space: space.id,
81
+ body: { text: 'Hello, decentralized world!' },
82
+ proof: ucan.encoded,
83
+ }),
84
+ session.privateKey,
85
+ );
86
+ await storage.addExpression(signed);
87
+ ```
88
+
89
+ Syncing it to other devices is a network manager plus a sync engine with the
90
+ validation engine in front — see *Sync* below. Or skip all of this and use a
91
+ node, which does the wiring for you — next.
92
+
93
+ ## The node — start here
94
+
95
+ Most applications never touch the modules below directly. `createNode` wires an
96
+ identity, its spaces, validation, encryption and sync into one object, and its
97
+ API is plain data in and out:
98
+
99
+ ```typescript
100
+ import { createNode, createIdentityManager, createLocalRootSigner, indexedDBStores, rolePresets } from '@weaveprotocol/core';
101
+
102
+ const manager = createIdentityManager();
103
+ const me = await manager.fromRecoveryCode(code);
104
+
105
+ const node = await createNode({
106
+ signer: createLocalRootSigner(me, manager.getProvider()), // or anything that signs
107
+ stores: indexedDBStores('my-app'), // or folderStores(directory, …)
108
+ network: { relays: ['wss://relay.example'] },
109
+ });
110
+
111
+ const space = await node.spaces.create({ name: 'Groceries', visibility: 'private', ...rolePresets.team });
112
+ const milk = await node.records.put(space.id, 'app.todo.item', { text: 'milk', done: false });
113
+ await node.records.update(space.id, milk.key, { text: 'milk', done: true }); // same key, next version
114
+ node.subscribe((event) => { if (event.type === 'records') redraw(); });
115
+
116
+ const invite = await node.spaces.invite(space.id); // a friend calls node.spaces.join(invite) — and joins as an Editor
117
+ const view = await node.spaces.invite(space.id, { write: false }); // they can read, not change
118
+ await node.spaces.closeInvite(space.id, invite); // nobody else joins with that link
119
+ ```
120
+
121
+ What it takes care of:
122
+
123
+ - **One root signature an hour.** The node signs with a session key and asks the
124
+ root signer for a fresh delegation before the old one runs out.
125
+ - **A record keeps its key; edits are versions.** `update` writes the next
126
+ version — same key, `seq` one higher, `prev` naming the version it replaces —
127
+ and `delete` writes a version marked deleted. Which version is current is
128
+ decided by `seq`, then id, never by a clock: a replayed old version cannot
129
+ roll a record back, a delete stays deleted, and two devices that edited apart
130
+ agree on the winner. Only the current version is kept, unless a collection
131
+ is defined with `history: 'all'`, which keeps every version as a hash-linked
132
+ chain (`records.history`). Anyone who may write in a space may edit and delete
133
+ in it.
134
+ - **Records outlive their session.** A delegation is judged at the moment a
135
+ record was signed, so a peer arriving next week still accepts last week's data.
136
+ - **Unknown collections are kept.** Records in collections the node has no
137
+ schema for are stored and synced on the strength of their signature and
138
+ capability, so an always-on node — or an agent inventing a collection — does
139
+ not need every app's schema.
140
+
141
+ Every operation is also described in `NODE_ACTIONS` — a name, a sentence and a
142
+ JSON Schema for its input — which is what the CLI, MCP and WebMCP front ends are
143
+ generated from. `runAction(node, 'records_put', { … })` runs one by name.
144
+
145
+ ## Signing in — the element, and React
146
+
147
+ Getting to a node takes a sign-in flow: where the data lives (a pod or this
148
+ browser), which account, the ways into it (its password, a passkey, a device
149
+ password), creating one, staying signed in, arriving from a phone-pairing QR.
150
+ The protocol ships it, so an app does not write it:
151
+
152
+ ```html
153
+ <weave-auth app-name="Todo" relays="wss://relay.example"></weave-auth>
154
+ <script type="module">
155
+ import '@weaveprotocol/core/elements';
156
+ document.querySelector('weave-auth').addEventListener('weave-session', (event) => {
157
+ const session = event.detail.session; // { account, did, sessionDid, node }, or null
158
+ if (session) start(session.node);
159
+ });
160
+ </script>
161
+ ```
162
+
163
+ The element fits whatever it is put in — a page, a modal, a side panel — by
164
+ sizing to its container, and draws nothing once someone is in. It renders into
165
+ the page rather than a shadow root, because password managers fill forms there
166
+ reliably and the account password living in one is the point. Colours, font and
167
+ radius are custom properties (`--weave-accent`, `--weave-font`, …).
168
+
169
+ Underneath it is `createWeaveAuth` (`@weaveprotocol/core/session`): the same flow as
170
+ state and actions, with no framework. The element draws it; an app that wants
171
+ its own screens draws it itself. Either way the seed stays inside it.
172
+
173
+ ```tsx
174
+ import { createWeaveAuth } from '@weaveprotocol/core/session';
175
+ import { WeaveProvider, WeaveAuth, useWeave, useNode, useQuery } from '@weaveprotocol/core/react';
176
+
177
+ const auth = createWeaveAuth({ appName: 'Todo', network: { relays: ['wss://relay.example'] } });
178
+
179
+ createRoot(root).render(
180
+ <WeaveProvider auth={auth}>
181
+ <App />
182
+ </WeaveProvider>,
183
+ );
184
+
185
+ function App() {
186
+ const { state } = useWeave();
187
+ if (state?.stage !== 'ready') return <WeaveAuth />;
188
+ return <Todos space={…} />;
189
+ }
190
+
191
+ function Todos({ space }) {
192
+ // Re-renders as records change here or arrive from peers.
193
+ const { result } = useQuery(space, { collection: 'app.todo.item', sort: { '@createdAt': 'asc' } });
194
+ const node = useNode();
195
+ const add = (text) => node.records.put(space, 'app.todo.item', { text, done: false });
196
+ …
197
+ }
198
+ ```
199
+
200
+ Everything below the provider asks for what it needs:
201
+
202
+ | Hook | Gives |
203
+ |---|---|
204
+ | `useWeave()` | The flow, its state and the session — or nulls, before sign-in |
205
+ | `useAuth()` / `useSession()` / `useNode()` | The same, for components that only exist once someone is in |
206
+ | `useSpaces()` | The account's spaces, kept current, with `create`, `join`, `leave` |
207
+ | `useQuery(space, query)` | Records matching a query, kept current |
208
+ | `useRecord(space, key)` / `useLinked(space, key)` | One record; what points at it |
209
+ | `useCollections(space)` / `useProfiles(space)` / `useSpaceStatus(space)` | What a space holds, who is in it, whether it is connected |
210
+ | `useCan(space, action, target)` | Whether this account may create, edit or delete — for hiding a button |
211
+ | `useOpenSpace(space)` | Keeps a space syncing while a view is on screen |
212
+ | `useLive(space, load, deps)` | Anything else, reloaded as the space changes |
213
+
214
+ An app connected to an account home passes its node instead:
215
+ `<WeaveProvider node={node}>`. React is an optional peer dependency; only
216
+ `@weaveprotocol/core/react` imports it.
217
+
218
+ ## Apps without the seed — the account home
219
+
220
+ An app does not have to sign anyone in at all. It can ask an **account home** —
221
+ a page, at an address the person chose, that holds their account — for access,
222
+ and never see the seed. `home/` is one, ready to deploy as your own:
223
+
224
+ [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/leifriksheim/weave&base=home)
225
+ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/leifriksheim/weave&root-directory=home)
226
+
227
+ An app connects with `createWeaveConnection` — the twin of `createWeaveAuth`,
228
+ for apps:
229
+
230
+ ```tsx
231
+ import { createWeaveConnection } from '@weaveprotocol/core/session';
232
+ import { WeaveProvider, useConnection } from '@weaveprotocol/core/react';
233
+
234
+ const connection = createWeaveConnection({
235
+ home: 'https://weave-home.netlify.app/connect',
236
+ request: {
237
+ name: 'Todo',
238
+ access: 'write', // or 'read'
239
+ scope: 'spaces', // or 'account': every space
240
+ create: [{ name: 'Todos', visibility: 'private' }], // made by the home, in the account
241
+ },
242
+ network: { relays },
243
+ });
244
+
245
+ <WeaveProvider connection={connection}><App /></WeaveProvider>;
246
+
247
+ function App() {
248
+ const { connection, state } = useConnection();
249
+ if (state.status !== 'ready') return <button onClick={() => connection.connect()}>Connect with Weave</button>;
250
+ return <Todos />; // useNode(), useQuery(…) — the same hooks as anywhere
251
+ }
252
+ ```
253
+
254
+ It remembers the grant between visits, starts the node from it, and says
255
+ `expired` when the note runs out; connecting again renews it.
256
+
257
+ The `home` an app names is only a suggestion. The home belongs to the person:
258
+ `connection.connect('weave.example.com')` uses their own, and the app remembers
259
+ it — for reconnecting and for "account settings". The grant carries the home's
260
+ relays, and the app joins them, so an app and a home configured with different
261
+ relays still meet. Underneath are
262
+ `connectToHome`, `startConnectedNode` and `grantStore`, for apps without React.
263
+
264
+ 1. The app makes its own key, kept in its own site's storage and never
265
+ exportable (`appKey()`).
266
+ 2. The home opens in a popup. The person unlocks there — the account password
267
+ from their password manager, or a passkey — and picks which spaces the app
268
+ gets.
269
+ 3. The home signs a note from the account to the app's key: these spaces, read
270
+ or change, for seven days. It hands the note back with invites for those
271
+ spaces, to the app's origin only.
272
+ 4. The app's node signs with its own key under that note. It acts *for* the
273
+ account — records show the account as their author — but every peer checks
274
+ the note, so it cannot write anywhere it was not given.
275
+
276
+ What the note limits: **writing**, per space, checked by every peer. What it
277
+ cannot limit: **reading** a private space it was given — whoever holds a space's
278
+ key can read all of it, and that key does not change yet. Spaces an app wants
279
+ for itself are created by the home, as part of the approval, so they land in
280
+ the account's list on every device.
281
+
282
+ An app that is a view onto *everything* — like the example — asks for
283
+ `scope: 'account'`: a note for every space, plus the key the account's space
284
+ list is derived from, so it sees every space and can make and join them. It
285
+ still never holds the seed: it cannot sign in anywhere as the account, change
286
+ its password or passkeys, or keep access past the note's date.
287
+
288
+ The home side is `receiveConnectRequest()` and `auth.grant(…)`; see
289
+ [home/README.md](home/README.md).
290
+
291
+ ## Modules
292
+
293
+ ### Identity (`@weaveprotocol/core/identity`)
294
+
295
+ | Export | Description |
296
+ |--------|-------------|
297
+ | `generateSeed()` / `seedToRecoveryCode()` / `recoveryCodeToSeed()` | The account seed and its written form |
298
+ | `createIdentityManager()` | `fromSeed`, `fromRecoveryCode`, `fromPassword`; passkey-PRF derivation as an option |
299
+ | `createLocalRootSigner()` | A `RootSigner` for a seed unlocked in this page |
300
+ | `createFolderAccountStore()` / `createBrowserAccountStore()` | Where accounts live: a data folder, or this browser |
301
+ | `wrapSeedWithDeviceKey()` / `wrapSeedWithPassphrase()` | Local ways to unlock a stored seed |
302
+ | `deriveVaultKey()` | Key for sealing an account's space registry at rest |
303
+ | `pairingRoomId()` / `encodePairingTicket()` / `sealPairingPayload()` | Bringing a phone into an account |
304
+ | `issueUCAN()` / `verifyUCAN()` | Capability tokens (UCAN 0.10, `ES256` JWTs) |
305
+ | `delegateCapabilities()` | Attenuated delegation from a parent token |
306
+ | `validateDelegationChain()` | Verify a full root → … → leaf proof chain |
307
+ | `createP256Provider()` | ECDSA P-256 crypto provider (swappable) |
308
+ | `publicKeyToDid()` / `didToPublicKey()` | `did:key` encoding |
309
+
310
+ #### The account is a seed
311
+
312
+ An identity is 16 random bytes. HKDF-SHA256 stretches them to 48, `@noble/curves`
313
+ reduces those to a P-256 private key the standard way (FIPS 186-5, appendix
314
+ A.2), and the compressed public key becomes a spec-conformant `did:key`
315
+ (`did:key:zDn…`). The same seed always yields the same DID, and anything that
316
+ DID signs verifies for anyone who holds only the DID. Golden tests pin known
317
+ seeds to their DIDs, because a silent change here would give every account a
318
+ new identity.
319
+
320
+ The seed's written form is the **recovery code**: 128 bits in Crockford base32.
321
+ It is the primary way in, not a fallback. It is the only credential that works
322
+ on a domain that has never seen you, because there is nothing stored there for
323
+ anything else to unlock.
324
+
325
+ ```typescript
326
+ const code = generateRecoveryCode(); // 'K7N6-ERYP-68TZ-A7HN-VJW3-QWKN-CG'
327
+ const me = await createIdentityManager().fromRecoveryCode(code);
328
+ // case, spacing and the usual O/0, I/1 slips are all forgiven on the way back in
329
+ ```
330
+
331
+ To a password manager the code is an ordinary generated password, so Bitwarden,
332
+ 1Password, iCloud Keychain and the rest can store and autofill it. The example
333
+ app presents it in a username/password form for exactly that reason.
334
+
335
+ #### Unlocking on a device you've used before
336
+
337
+ Typing the code every visit would be tedious, so each origin can keep **wraps**:
338
+ encrypted copies of the seed, each opened a different way.
339
+
340
+ | Wrap | Opened by | Notes |
341
+ |---|---|---|
342
+ | `device` | A random, non-extractable key kept in this origin, with a passkey as the gate in front of it | Works with every passkey provider, because nothing is derived from the passkey |
343
+ | `passphrase` | PBKDF2-SHA256 → AES-GCM | A short password for this device |
344
+
345
+ See *Locking the folder* below for how wraps are stored.
346
+
347
+ #### Why passkeys are a gate, not the identity
348
+
349
+ A passkey can only hand an app a secret through the WebAuthn **PRF** extension.
350
+ Several major credential managers — Bitwarden and 1Password among them — store
351
+ passkeys without PRF, or report it inconsistently. An identity *derived* from a
352
+ passkey would lock those users out, and it would still be a different identity
353
+ on every domain, since a passkey is bound to one.
354
+
355
+ So the passkey only decides whether this origin may use its device key. PRF
356
+ derivation is still available (`identity.register()` / `authenticate()`, with
357
+ `inspectPasskeyPrf()` to diagnose what a provider actually does), but nothing in
358
+ the example depends on it.
359
+
360
+ #### UCAN delegation
361
+
362
+ A root identity — a passkey you never expose to a web app — delegates narrow,
363
+ expiring capabilities to keys that do the day-to-day signing:
364
+
365
+ ```typescript
366
+ import { issueUCAN, delegateCapabilities, validateDelegationChain } from '@weaveprotocol/core';
367
+
368
+ // Root grants a session key everything it may do with todos, for an hour
369
+ const sessionUcan = await issueUCAN({
370
+ issuer: { did: me.did, privateKey: me.privateKey },
371
+ audience: sessionDid,
372
+ capabilities: [{ with: 'space:app.example.todo', can: 'expression/*' }],
373
+ expiration: Math.floor(Date.now() / 1000) + 3600,
374
+ }, provider);
375
+
376
+ // The session key hands a guest a strictly weaker, read-only capability
377
+ const guestUcan = await delegateCapabilities({
378
+ parent: sessionUcan,
379
+ issuer: { did: sessionDid, privateKey: sessionKey },
380
+ audience: guestDid,
381
+ capabilities: [{ with: 'space:app.example.todo', can: 'expression/read' }],
382
+ }, provider);
383
+
384
+ // Any peer can check the whole chain back to the root DID
385
+ const chain = await validateDelegationChain(guestUcan.encoded, [sessionUcan.encoded], provider);
386
+ chain.valid; // true
387
+ ```
388
+
389
+ Escalation is refused at issue time (a child capability must be a subset of its
390
+ parent), a delegation can never outlive its parent, and only the audience of a
391
+ token may delegate it onward.
392
+
393
+ ### Schema (`@weaveprotocol/core/schema`)
394
+
395
+ Typed, signed data expressions using [Standard Schema](https://standardschema.dev/).
396
+
397
+ | Export | Description |
398
+ |--------|-------------|
399
+ | `createSchemaEngine()` | Register collections with Standard Schema validators |
400
+ | `validateJsonSchema()` / `asStandardSchema()` | The JSON Schema a space stores, and its Standard Schema adapter |
401
+ | `createSigner()` | Sign and verify expressions (JWS-style) |
402
+ | `createExpression()` | Build unsigned expressions (optionally carrying a UCAN `proof`) |
403
+ | `canonicalize()` | Deterministic JSON serialization |
404
+
405
+ ### Storage (`@weaveprotocol/core/storage`)
406
+
407
+ Local-first storage with Merkle Search Tree for efficient sync.
408
+
409
+ | Export | Description |
410
+ |--------|-------------|
411
+ | `createStorageProvider()` | MST-backed expression storage; `compact()` deletes tree nodes the root no longer reaches |
412
+ | `createIndexedDBAdapter()` | IndexedDB storage adapter, scoped to this origin |
413
+ | `createFolderAdapter()` | A user-picked directory, shared by every origin given access |
414
+ | `createEncryptedAdapter()` | Seals chosen keys (space records, space keys) at rest |
415
+ | `reconcileFolder()` | Rebuilds the tree after another writer touched a folder |
416
+ | `insertIntoMST()` / `listMSTEntries()` | Direct MST operations; a listing can stop at a key prefix |
417
+
418
+ ### Spaces
419
+
420
+ A space is the container everything else lives in. It is **public** (signed in
421
+ the clear) or **private** (every body encrypted with the space key), and who
422
+ may write in it is decided by its **roles** — the space's own, not the
423
+ protocol's. A private notebook is a space whose creator never invited anyone;
424
+ a team list is one where everyone invited holds an Editor role.
425
+
426
+ | Export | Description |
427
+ |--------|-------------|
428
+ | `createSpaceManager()` | Create, list, join and forget spaces; mint invites |
429
+ | `parseSpaceInvite()` | Read an invite without joining, to show what it offers |
430
+ | `checkSpace()` / `spaceIdOf()` | Whether a space you were handed is the one its id names |
431
+ | `rolePresets` | Starting roles to use or ignore: `solo`, `team`, `community` |
432
+ | `replayAccess()` | The access history, replayed — who holds what, as of any point |
433
+ | `deriveInviteKey()` / `deriveReadKey()` | An invite link's key, and a private space's read key |
434
+
435
+ ```typescript
436
+ const spaces = createSpaceManager(adapter);
437
+
438
+ const { space, key } = await spaces.create({
439
+ name: 'Move house',
440
+ visibility: 'private', // key generated, bodies encrypted
441
+ creator: me.did,
442
+ ...rolePresets.team, // Owner, and Editor for whoever is invited
443
+ });
444
+ ```
445
+
446
+ Give each space its own storage and its own MST and a peer you share one list
447
+ with learns nothing about the others.
448
+
449
+ #### Who may write: roles, and a history every peer replays
450
+
451
+ Three questions decide every write, each with its own mechanism:
452
+
453
+ 1. **Who is really writing?** A note (UCAN): "this key speaks for this account."
454
+ 2. **What standing does that account have here?** Its **role** in the space.
455
+ 3. **Does this action on this record allow it?** The collection's **rules**.
456
+
457
+ A **role** is a name, a rank and a list of permissions. Three permissions are
458
+ the protocol's own — `manage` (roles and members), `invite` and `define`
459
+ (collections) — and every other one belongs to a collection (`app.poll/moderate`).
460
+ A `*` matches anything: `*` is every permission, `*/*` every collection's. The
461
+ **rank rule** is the only check rules cannot express: you may change people
462
+ and roles ranked below you, and give out roles up to your own rank. Two people
463
+ at the same rank can never remove each other, only themselves — so the creator
464
+ **hands over** by giving someone their role, then leaving, and the space goes on.
465
+
466
+ Roles, members, invites, revoked notes and collection definitions are records
467
+ (`sys.role`, `sys.member`, `sys.invite`, `sys.revoke`, `sys.collection`), and
468
+ every record written anywhere names the latest of them its writer knew, as
469
+ `seen`. That makes the **access history** a small graph, which every peer
470
+ replays the same way (`space/roles.ts`): a change comes after what it saw;
471
+ changes that did not see each other go taking-away first — counting everything
472
+ on the way to one — then the higher-ranked author, then the lower id; a change
473
+ counts only if its author had the power both as of what they saw and at its
474
+ turn. A record is judged by its author's role, and the definition in force, as
475
+ of its own `seen`.
476
+
477
+ **Taking access back.** Removing someone, lowering a role or closing an invite
478
+ carries a **keep list**: the records the remover had seen. A record that relied
479
+ on what was taken away, and had not seen it go, stands only if it is kept — so
480
+ claiming an old point in history, or an old date, gets a removed member
481
+ nothing, while what they wrote before stays. Apps connected through an account
482
+ home write under a note, never a secret of the space's; **Disconnect** writes a
483
+ `sys.revoke` for that note, and nothing under it counts from then on except
484
+ what the home had seen.
485
+
486
+ **Invites** are one per role. `node.spaces.invite(space)` opens one for the
487
+ lowest role below yours — or makes a view-only one when there is none — and
488
+ the link carries its secret (and a private space's key): shown once, kept
489
+ nowhere. The joiner writes their own member record, signed a second time by
490
+ the invite's key over the space and their identity; it counts once the
491
+ invite's record has reached them, so joining finishes on the first sync.
492
+ `closeInvite` takes the link itself.
493
+
494
+ Roles, members, invites and revokes stay **in the clear**, even in a private
495
+ space: a relay, a mirror or a host holding no secret of the space's replays the
496
+ same history and reaches the same verdict as a member, so a stranger who knows
497
+ a space's id cannot get a record stored anywhere. That shows who holds which
498
+ role — DIDs are on every signed record anyway. Collection definitions stay
499
+ sealed.
500
+
501
+ A private space also has a **read key**, derived from the space key, so everyone
502
+ who can read has it. Every connection — to an always-on node, or peer to peer
503
+ through a relay — starts with a handshake before anything else crosses it:
504
+ each side signs a fresh challenge with the key its DID names, so nobody can
505
+ connect under someone else's name, and in a private space with the read key
506
+ too, checked against its public half. A stranger who learns a space's id, or a
507
+ relay that sees its room, gets no ciphertext. A peer-to-peer handshake also
508
+ signs both ends' DTLS fingerprints, so a relay that swapped in its own offer to
509
+ sit in the middle is caught. Roles govern writing only: someone removed keeps
510
+ the read key until the space's key changes for everyone (BLOCK-14 §2).
511
+
512
+ A space's **id is the hash of what is fixed at creation**: creator, visibility,
513
+ starting roles and which one the creator holds, time, a random nonce and the
514
+ read key (the name is left out, so it can change). `join` refuses an invite
515
+ whose space does not hash to its id, or whose key is not the one the space
516
+ names — so whoever passes an invite on cannot change who started the space, or
517
+ with which roles.
518
+
519
+ **Spaces describe themselves.** A space stores its collections' definitions —
520
+ name, title, description and a JSON Schema — as signed records in
521
+ `sys.collection`, so an app or an agent that has never seen a space can ask what
522
+ it holds (`node.collections.list`) and what each thing looks like. Records are
523
+ checked against the definition when written, and flagged (`conforms`) when read;
524
+ nothing is refused during sync for its shape, so peers that saw definitions in
525
+ different orders still converge. `node.collections.define` publishes one — the
526
+ same call an agent makes through MCP.
527
+
528
+ **Links.** A record can point at another in a named role — `{ rel: 'about',
529
+ to: <key> }` — and `node.records.linked(space, key)` answers what points at a
530
+ thing. Links point at keys, so a comment stays on a post however often the post
531
+ is edited; in a private space they are sealed with the body, so a relay cannot
532
+ see what points at what. Collections declare their links in their definition,
533
+ so an agent reading `collections_list` sees how a space's things connect.
534
+
535
+ **Standard schemas, optional.** The protocol has no built-in kinds of record.
536
+ For the patterns nearly every app needs there is a small library of ordinary
537
+ collection definitions, named `std.*`, in `@weaveprotocol/core/schemas`: things that
538
+ attach to any record (`reaction`, `comment`, `tag`, `attachment`, `reference`)
539
+ and a few common nouns (`message`, `task`, `column`, `poll`, `vote`). Nouns kept in a hand-made
540
+ order carry a `position` string; `positionBetween(a, b)` makes one between two
541
+ neighbours, so moving a card rewrites only that card:
542
+
543
+ ```typescript
544
+ import { reaction, useSchemas } from '@weaveprotocol/core/schemas';
545
+
546
+ await useSchemas(node, space.id, [reaction]); // defines only what the space lacks
547
+ await node.records.put(space.id, reaction.name, { emoji: '👍' }, { links: [{ rel: 'about', to: post.key }] });
548
+ ```
549
+
550
+ Using the same ones is how two apps agree — reactions from one show up in the
551
+ other. The example app's Apps tab is built on this: a chat, a kanban board and
552
+ polls, each appearing in a space once it holds the collections it needs
553
+ (`std.message`; `std.task` and `std.column`; `std.poll` and `std.vote`). An app that wants its own shape defines its own collection instead.
554
+
555
+ **Rules, enforced by every peer.** A definition can say who may create, edit and
556
+ delete its records, what must be unique, and which fields are fixed:
557
+
558
+ ```typescript
559
+ await node.collections.define(space.id, {
560
+ name: 'app.poll.vote',
561
+ schema: voteSchema,
562
+ links: { about: { to: ['app.poll'], cardinality: 'one' } },
563
+ rules: { edit: 'creator', onePer: ['@author', 'link:about'] }, // one vote per person per poll
564
+ });
565
+ ```
566
+
567
+ `create`/`edit`/`delete` take `member` (anyone holding a role), `creator` — a
568
+ fact about the record, which nobody decides — or `can:<permission>`, naming a
569
+ permission the collection declares in `permissions`. The test for which: did a
570
+ person have to decide it? "The creator edits" follows from the data; "moderators
571
+ delete" needs `can:moderate`, and the space decides which roles hold
572
+ `app.poll/moderate`. `onePer` derives the record's key from what must be
573
+ unique, so voting again *is* changing your vote — no peer ever needs to see every
574
+ vote to stop a second one. `fixed` fields keep their first value. Each version
575
+ is judged by the definition in force as of the access history it saw, so every
576
+ peer judges it by the same rules: a forged edit or a second vote is refused
577
+ during sync, and something that arrives before what it depends on waits instead
578
+ of being guessed about. `node.records.can(space,
579
+ 'edit', key)` asks first — for hiding a button rather than showing an error.
580
+
581
+ **Queries.** `node.records.query(space, { collection, where, include,
582
+ sort, limit, cursor })` finds records with Mongo-style filters (`{ done: false,
583
+ amount: { $gt: 10 } }`; `@author`, `@createdAt` and friends for the record
584
+ itself) and pulls in what links to them — `include: { likes: { rel: 'about',
585
+ from: 'std.reaction', count: true } }`. A query is plain JSON, so an agent
586
+ sends the same thing over `records_query`; `node.records.watch` re-runs one as
587
+ records sync in. Only records this device can read come back, so `body` is
588
+ never null.
589
+
590
+ **Typed queries.** Name a collection by its definition instead of a string, and
591
+ the results are typed from its schema — including everything `include` pulls
592
+ in, and `number` for a `count`. `collection()` keeps a definition's types; the
593
+ standard schemas already carry theirs; `Typed<T>` names a collection whose
594
+ schema is plain JSON Schema. Before a query runs, every reference becomes its
595
+ name, so the query is still plain data.
596
+
597
+ ```typescript
598
+ import { collection } from '@weaveprotocol/core';
599
+
600
+ const polls = collection({ name: 'app.poll', schema: Poll }); // Poll is a Zod object
601
+ const votes = collection({ name: 'app.poll.vote', schema: Vote });
602
+
603
+ await node.records.put(space.id, polls, { question: 'Where?', options: ['Oslo', 'Lisbon'] }); // checked against Poll
604
+
605
+ const { records } = await node.records.query(space.id, {
606
+ collection: polls,
607
+ include: { votes: { rel: 'about', from: votes } },
608
+ });
609
+ records[0].body.question; // string
610
+ records[0].included.votes[0].body.choice; // number
611
+ ```
612
+
613
+ **The account registry.** Which spaces an account belongs to is itself kept in
614
+ a space: a private one whose id and key are derived from the account's vault
615
+ key, so every device of the account finds it and nobody else can. Creating or
616
+ joining a space writes a membership record there (carrying the invite, so the
617
+ key too); every other device and node of the account syncs it and joins by
618
+ itself. A membership deleted on any device means the account left, and every device leaves.
619
+ Pass `accountKey` to `createNode` to turn it on. The account's name lives there
620
+ too (`node.account.setName`), so a rename on one device or site reaches every
621
+ other one — and a site opening the account for the first time shows its name.
622
+
623
+ **Profiles.** Other people see you by that name. The node publishes it into
624
+ every space it opens, and again on a rename, as a `sys.profile` record keyed by
625
+ a hash of your identity; `node.spaces.profiles(space)` (and the
626
+ `spaces_profiles` action) says who is who. Every version is kept and the one
627
+ shown is the newest signed by the identity the key names, so nobody can rename
628
+ anyone else. Someone following a space without a role publishes nothing there.
629
+
630
+ **Moving and merging.** `copyAccountData` copies an account's spaces, keys and
631
+ records from one set of stores to another — out of a browser's own database into
632
+ a data folder, for instance. Because every record is signed, named by its
633
+ content, and deletes are records too, merging into a folder that already holds
634
+ the same account is the same operation: the result is everything from both, and
635
+ whatever either side deleted stays deleted. A space lives on the devices that hold it,
636
+ not inside the identity — bringing a DID back on a new device restores who you
637
+ are, and an invite (even one you send yourself) restores what you had. Expressions name their space in a signed
638
+ field, which stops one being replayed into another.
639
+
640
+ **Encrypt, then sign.** A private space encrypts the body *before* the expression
641
+ is signed, so the signature covers the ciphertext: peers without the key still
642
+ verify and relay the data, they simply cannot read it. The structural gate steps
643
+ aside for encrypted bodies — their shape is checked by members after decryption.
644
+
645
+ ### Network (`@weaveprotocol/core/network`)
646
+
647
+ Browser-to-browser communication via WebRTC.
648
+
649
+ | Export | Description |
650
+ |--------|-------------|
651
+ | `createMesh()` | One node's WebRTC connections through relays, shared by every space: `mesh.join(room, auth)` gives a space its peers |
652
+ | `createNetworkManager()` | Peers over a transport that dials on its own — a node's socket, a local link |
653
+ | `createSignalingClient()` | WebSocket signaling for ICE/SDP exchange |
654
+ | `createMultiSignalingClient()` | Several relays used at once, de-duplicated |
655
+ | `createRTCTransport()` | WebRTC data channel management (the default transport) |
656
+ | `createWebSocketTransport()` | A socket to one always-on node — no relay, no TURN |
657
+ | `createMeshAuth()` | The peer-to-peer handshake: each side proves its DID, and in a private space that it may read |
658
+ | `createClientAuth()` / `createServerAuth()` | The handshake with a node: the client proves its DID (and the read key, if private), the node signs with its own |
659
+
660
+ #### Signaling relay
661
+
662
+ `server/signaling-server.mjs` is a dumb relay in a couple hundred lines of
663
+ Node on the `ws` library: a peer holds one socket and joins a room on it for
664
+ each space, and the relay passes join notices and WebRTC offers, answers and
665
+ candidates between peers that share a room. (A socket opened with `?room=` is
666
+ the older one-room form, still served.) The room is a hash of
667
+ the space's id (`relayRoom`), so the relay cannot tell which space a room is.
668
+ Expression data never touches it — that flows peer to peer — and it cannot
669
+ read a private space.
670
+
671
+ It is open to anyone, so it keeps to limits: small messages, a cap on
672
+ connections per address, peers per room and rooms per socket, a message rate
673
+ per socket, and one DID per socket, which cannot be claimed twice in a room. Every message
674
+ it forwards carries the sender's DID as it joined, whatever the message says.
675
+
676
+ ```bash
677
+ npm run signal # ws://localhost:8787; /health says {"ok":true}
678
+ ```
679
+
680
+ Only peers already in a room hear about a newcomer, so exactly one side creates
681
+ the offer and the two never collide.
682
+
683
+ ### Sync (`@weaveprotocol/core/sync`)
684
+
685
+ Anti-entropy gossip protocol for eventual consistency.
686
+
687
+ | Export | Description |
688
+ |--------|-------------|
689
+ | `createSyncEngine()` | Automatic MST reconciliation with heartbeat |
690
+ | `verifyNode()` / `unknownChildren()` | The pieces of a tree walk |
691
+
692
+ Two peers compare roots — equal means identical, one round trip. Otherwise each
693
+ walks the other's tree from the root, skipping every subtree already in its own,
694
+ so cost follows the size of the difference: one changed entry in 10,000 costs
695
+ about 37 KB on the wire, where sending every key cost 508 KB. Whether a subtree
696
+ is already here is one lookup in the store, not a read of the whole local tree.
697
+
698
+ The engine's `validate` hook is the seam where the validation engine sits.
699
+ Expressions a peer sends are only committed if it accepts them; the rest are
700
+ dropped and surface as a `rejected` event with the reason.
701
+
702
+ ### Validation (`@weaveprotocol/core/validation`)
703
+
704
+ A pipeline of gates for incoming expressions.
705
+
706
+ | Export | Description |
707
+ |--------|-------------|
708
+ | `createValidationEngine()` | Full gatekeeper pipeline |
709
+ | `createCryptoGate()` | Expression id + signature verification |
710
+ | `createStructuralGate()` | Schema conformance via Standard Schema |
711
+ | `createCapabilityGate()` | UCAN authorization: may this key write this? |
712
+ | `createStatefulGate()` | Custom Wasm rules |
713
+
714
+ The crypto gate settles *who* signed an expression. The capability gate answers
715
+ the next question: were they allowed to? An expression signed by a delegated key
716
+ carries its UCAN in `proof` — a signed field, so it cannot be swapped out — and
717
+ the gate walks that chain back to a root identity, rejecting anything expired,
718
+ issued to a different key, broader than its parent, or rooted in an identity the
719
+ application does not trust.
720
+
721
+ ```typescript
722
+ const validation = createValidationEngine({
723
+ cryptoGate: createCryptoGate(provider),
724
+ structuralGate: createStructuralGate(schema),
725
+ statefulGate: createStatefulGate(),
726
+ capabilityGate: createCapabilityGate({
727
+ provider,
728
+ requiredCapability: (expression) => ({ with: `space:${expression.collection}`, can: 'expression/write' }),
729
+ isTrustedRoot: (did) => spaceMembers.has(did),
730
+ }),
731
+ resolvePublicKey: async (did) => provider.importPublicKey(didToPublicKey(did).publicKeyBytes),
732
+ getExpression: (id) => storage.getExpression(id),
733
+ });
734
+ ```
735
+
736
+ ### Privacy (`@weaveprotocol/core/privacy`)
737
+
738
+ End-to-end encryption for private Spaces.
739
+
740
+ | Export | Description |
741
+ |--------|-------------|
742
+ | `createPrivacyGuard()` | Transparent E2EE orchestrator |
743
+ | `generateSpaceKey()` | AES-GCM-256 space keys |
744
+ | `wrapSpaceKey()` | ECDH + AES-KW key distribution |
745
+
746
+ ## Storage Adapters
747
+
748
+ The protocol uses an adapter pattern for storage flexibility:
749
+
750
+ ```typescript
751
+ interface StorageAdapter {
752
+ get(key: string): Promise<Uint8Array | null>;
753
+ put(key: string, value: Uint8Array): Promise<void>;
754
+ delete(key: string): Promise<void>;
755
+ putExpression(expression: Expression): Promise<void>;
756
+ getExpression(id: string): Promise<Expression | null>;
757
+ deleteExpression(id: string): Promise<void>;
758
+ queryExpressions(collection: string, limit?: number): Promise<Expression[]>;
759
+ // ... more
760
+ }
761
+ ```
762
+
763
+ **Built-in**:
764
+
765
+ - `createIndexedDBAdapter(name)` — works in every browser. Origin-scoped.
766
+ - `createFolderAdapter(directory, namespace)` — a directory the user picked, via the File System Access API. **Not** origin-scoped. Chrome, Edge and Opera on the desktop.
767
+
768
+ The always-on node (`weave run`) uses the folder adapter on disk, in the same layout. **Planned**: mirrors, which keep a space in storage the user already pays for (a Dropbox app folder, Drive, S3) and sync with it like a peer — see `docs/blocks/BLOCK-03-mirrors.md`. OPFS is not on the list: it is origin-private, so it would inherit exactly the limitation a data folder exists to avoid.
769
+
770
+ ### Data folders — storage that outlives the origin
771
+
772
+ (The app calls a data folder a **pod**.)
773
+
774
+ Every in-browser store is keyed by origin. IndexedDB, localStorage, Cache API and OPFS (the name is the spec: *Origin Private* File System) all partition by it, so two deployments of one app on two domains can never read each other's data, and a passkey — bound to an RP ID, which is a domain — derives a different identity on each. Two views of the same app become two unrelated accounts.
775
+
776
+ A directory handle is the exception. Each origin asks for permission once, and both end up looking at the same files:
777
+
778
+ ```
779
+ <folder>/
780
+ accounts.json name, DID and id of each account (readable without unlocking)
781
+ accounts/<id>/account.json that account's seed, encrypted once per way of unlocking it
782
+ accounts/<id>/stores/<namespace>/
783
+ kv/<key> MST nodes, the root pointer, space records (sealed)
784
+ expressions/<cid>.json one signed record per file
785
+ ```
786
+
787
+ A folder is a disk, not a person: several accounts can live in one. A browser
788
+ with no folder keeps the same shape in IndexedDB, so an app has one model
789
+ rather than two.
790
+
791
+ ```typescript
792
+ import {
793
+ pickDataFolder, createFolderAccountStore, recoveryCodeToSeed, deriveVaultKey,
794
+ createFolderAdapter, createEncryptedAdapter, reconcileFolder,
795
+ createIdentityManager, createStorageProvider,
796
+ } from '@weaveprotocol/core';
797
+
798
+ const folder = await pickDataFolder(); // needs a user gesture
799
+ const accounts = createFolderAccountStore(folder);
800
+ const [account] = await accounts.list(); // names and DIDs; nothing unlocked yet
801
+
802
+ const seed = recoveryCodeToSeed(code); // or open one of its wraps, below
803
+ const identity = await createIdentityManager().fromSeed(seed);
804
+
805
+ const adapter = await createFolderAdapter(folder, `${account.dataPath}/spaces/${spaceId}`);
806
+ const storage = createStorageProvider(adapter);
807
+ await reconcileFolder(storage, adapter); // pick up other writers
808
+
809
+ // The registry is sealed under a key only an unlocked folder can derive.
810
+ const registry = createEncryptedAdapter(
811
+ await createFolderAdapter(folder, `${account.dataPath}/registry`),
812
+ await deriveVaultKey(seed),
813
+ );
814
+ ```
815
+
816
+ **Expressions are the truth; the MST is an index over them.** That inversion is what lets several writers share one folder without taking a lock. Every expression file is named by its own content hash, so concurrent writers can only ever add files that agree; the single mutable thing, the root pointer, is derived state that either side can rebuild. `reconcileFolder()` rebuilds it — call it on an interval, on window focus, or after a sync round, since the web has no filesystem change notification.
817
+
818
+ Two consequences worth having:
819
+
820
+ - **The folder is the account.** Copy it to a USB stick and it is your whole identity. Put it in iCloud, Dropbox or Syncthing and several devices converge with no relay at all — the folder becomes a second transport alongside WebRTC, and both meet in the same anti-entropy merge.
821
+ - **It changes nothing about the mesh.** A folder-backed node is an ordinary peer that happens to be durable and readable by several origins — an availability role, never an authority one. Where there is no folder (Safari, Firefox, mobile) a node keeps an origin-scoped replica and gossips exactly as before.
822
+
823
+ ### Locking the folder
824
+
825
+ A folder whose account file held the seed in the clear would be a bearer token — copying it would be enough to become its owner, and the AES key for every private space sits in the same directory as the ciphertext it opens. So the seed is never stored. Each `account.json` holds **wrapped copies** of it, one per way of unlocking:
826
+
827
+ ```
828
+ account seed (16 bytes, never written in the clear)
829
+ ├── HKDF → vault key ──encrypts──> space keys and space records at rest
830
+ └── stored only as wraps:
831
+ device a random local key, gated by any passkey (one per origin)
832
+ passphrase PBKDF2-SHA256 → AES-GCM
833
+ ```
834
+
835
+ A device wrap is opened by a random key kept in one origin's storage, with a passkey as the gate in front of it — **not** derived from the passkey (see *Why passkeys are a gate, not the identity*). So every provider works, and each origin adds a wrap of its own:
836
+
837
+ ```typescript
838
+ const deviceKey = await createDeviceKey(); // non-extractable, local
839
+ const wrap = await wrapSeedWithDeviceKey(seed, deviceKey, { rpId, credentialId });
840
+ await accounts.write(account, withWrap(vault, wrap));
841
+ ```
842
+
843
+ `deviceWrapsFor(vault, rpId)` says which wraps this origin can even attempt; the rest name keys it cannot reach. The gate is enforced in application code rather than by cryptography — see `src/identity/device-key.ts` for what that does and does not protect against. The recovery code needs no wrap, because it *is* the seed in printable form — it opens the folder anywhere, including on a phone or in a browser with no File System Access API, and it is shown once and stored nowhere.
844
+
845
+ `createEncryptedAdapter` seals `space:`, `spacekey:`, `spaceinvite:` and `spacerole:` values under the vault key, which is what makes a private space genuinely unreadable to someone holding the folder. It is scoped deliberately narrowly: expressions and MST nodes pass through, so what stays legible is each record's author, timestamp and collection, plus anything in a space its owner made public. Sealing those too would mean an opaque blob store, which would cost the property that makes a folder worth having.
846
+
847
+ ### Where the root key lives
848
+
849
+ The root key signs exactly one thing: a note saying a session key may write for
850
+ the next hour. Everything else is signed by the session key. That one signature
851
+ is the only reason an app needs the identity — so it is the only thing that has
852
+ to move for the key to live somewhere else:
853
+
854
+ ```typescript
855
+ export interface RootSigner {
856
+ readonly did: string;
857
+ readonly custody: 'local' | 'remote';
858
+ delegate(params: { audience, capabilities, expiration }): Promise<UCANToken>;
859
+ }
860
+ ```
861
+
862
+ `createLocalRootSigner` signs in the page, for a seed unlocked here. Anything
863
+ else that holds the key can implement the same interface and sign where it is,
864
+ so the seed never crosses into the page. Nothing downstream changes either
865
+ way, because DIDs, expressions, validation and sync never see the root key
866
+ under any arrangement.
867
+
868
+ ### Meeting peers
869
+
870
+ Two browsers cannot find each other unaided — neither can accept an incoming
871
+ connection — so something has to make the introduction. That something is a
872
+ relay, and it is worth being precise about how little it is: it forwards
873
+ connection offers, never sees an expression, and drops out of the conversation
874
+ the moment two peers are talking.
875
+
876
+ Two things keep it from being an authority:
877
+
878
+ ```typescript
879
+ const mesh = createMesh({
880
+ // Used all at once, not as failover: two people who picked different relays
881
+ // would otherwise never meet.
882
+ relays: ['wss://relay-a.example', 'wss://relay-b.example'],
883
+ did: sessionDid,
884
+ introductions: true, // the default
885
+ });
886
+ const peers = mesh.join(await relayRoom(spaceId), createMeshAuth(spaceId, session, read, provider));
887
+ ```
888
+
889
+ **One connection per pair of devices**, however many spaces they share: one
890
+ socket per relay, one WebRTC connection per peer, and each space proves itself
891
+ on it separately before any of its data crosses — so a peer you share one
892
+ space with is a peer in that one only.
893
+
894
+ **Several relays**, so there is no single phone book — a peer announced by two
895
+ of them is announced upward once, and replies go back the way they arrived.
896
+
897
+ **Peers introduce peers**, so a relay is only needed for the *first* connection.
898
+ Once you are connected to someone, their data channel carries signalling for the
899
+ peers you have not met: `__peers` says who I can see, `__signal` carries
900
+ somebody else's offer onward, bounded by a hop count and de-duplicated by id.
901
+ Both sides of a new pair learn of each other at once, so the lower identifier
902
+ offers and the other waits — otherwise every introduction would open two
903
+ connections. After that the mesh introduces itself and the relay can go away.
904
+
905
+ `server/` has a Dockerfile and a `fly.toml` for running one.
906
+
907
+ ### Pairing a phone
908
+
909
+ No mobile browser has the File System Access API, so a phone keeps its own
910
+ replica like any other peer. Getting it started takes two things — the identity,
911
+ and the list of spaces — and only the first fits in a QR code:
912
+
913
+ ```typescript
914
+ import {
915
+ pairingRoomId, derivePairingKey, encodePairingTicket,
916
+ sealPairingPayload, openPairingPayload,
917
+ } from '@weaveprotocol/core';
918
+
919
+ // Desktop: a link for the QR. The fragment never reaches a server.
920
+ const ticket = encodePairingTicket({ v: 1, code: seedToRecoveryCode(seed), relay });
921
+ const url = `${origin}${pathname}#pair=${ticket}`;
922
+
923
+ // Both sides, independently — no negotiation, nothing sent.
924
+ const room = await pairingRoomId(seed);
925
+ const key = await derivePairingKey(seed);
926
+
927
+ // Desktop, once the phone turns up in that room:
928
+ send(await sealPairingPayload(utf8Encode(JSON.stringify({ spaces: invites })), key));
929
+ ```
930
+
931
+ The room is derived from the **seed**, not the DID. A DID appears in every
932
+ expression an account has ever signed, so a room named after one could be found
933
+ by anyone who had seen its data; a room named after the seed can only be found by
934
+ someone who already has it.
935
+
936
+ Encoding a URL rather than raw data means phone cameras open it natively — no
937
+ scanner, and it works on iOS. Afterwards the phone is a full peer that syncs with
938
+ anyone in the space, not a satellite of the machine that paired it.
939
+
940
+ ## Swappable Crypto
941
+
942
+ The `CryptoProvider` interface abstracts key algorithms:
943
+
944
+ ```typescript
945
+ // Default: ECDSA P-256
946
+ const provider = createP256Provider();
947
+
948
+ // Future: Ed25519, etc.
949
+ const identity = createIdentityManager({ provider: myEd25519Provider });
950
+ ```
951
+
952
+ ## Standard Schema Integration
953
+
954
+ A space stores its collections' shapes as JSON Schema, so any app in any
955
+ language can read them. You don't have to write it by hand: pass a validator
956
+ that can describe itself as JSON Schema — [Standard JSON
957
+ Schema](https://standardschema.dev/json-schema): Zod 4.2+, ArkType 2.1.28+,
958
+ Valibot through `toStandardJsonSchema` — and the node stores what it accepts.
959
+
960
+ ```typescript
961
+ import * as z from 'zod';
962
+
963
+ const Poll = z.object({
964
+ question: z.string().min(1).max(500),
965
+ options: z.array(z.string().min(1)).min(2).max(10),
966
+ });
967
+
968
+ await node.collections.define(space.id, { name: 'app.poll', schema: Poll });
969
+ ```
970
+
971
+ A space can store only a small subset of JSON Schema, the part every language
972
+ agrees on (types, required, enums, lengths and sizes, number bounds, labelled
973
+ choices). A validator feature with no stored equivalent — `z.email()`, whose
974
+ check is a regex — is refused when you define the collection, saying what is
975
+ supported, rather than quietly not being enforced by other apps.
976
+
977
+ Lower down, the schema engine takes any [Standard Schema
978
+ v1](https://standardschema.dev/) validator directly, for local checks:
979
+
980
+ ```typescript
981
+ import { createSchemaEngine } from '@weaveprotocol/core';
982
+
983
+ const schema = createSchemaEngine();
984
+ schema.registerCollection({ name: 'app.example.post', schema: PostSchema });
985
+ ```
986
+
987
+ ## Command line, always-on node, and agents
988
+
989
+ `cli/` is `weave`: every node operation from a terminal, `weave run` to keep an
990
+ account's spaces syncing on a server (browsers connect to it over WebSocket, and
991
+ it doubles as a relay), and `weave mcp` to hand the same operations to an agent.
992
+ It reads and writes the same data folder layout a browser does. See
993
+ [cli/README.md](cli/README.md).
994
+
995
+ ## Example app
996
+
997
+ `example/` is the Weave website — a landing page for developers at `/`, and
998
+ why Weave, for people, at `/why` — and, at `/app`, a general-purpose app for your spaces — Vite + React, consuming
999
+ the protocol straight from `src/`. It knows no kinds of data in advance: every
1000
+ screen is worked out from what a space says about itself (see *Derived UI* below):
1001
+
1002
+ ```bash
1003
+ npm install && (cd example && npm install) && (cd home && npm install) && (cd cli && npm install)
1004
+ npm run dev
1005
+ ```
1006
+
1007
+ That starts three things: the example app on 5173, the account home it
1008
+ connects to on 5174, and an always-on node on port 8787 that is also the relay.
1009
+ The node gets a throwaway identity on first run (`cli/.env.dev`, data in
1010
+ `.weave-dev/`), and `example/.env.development` points the app at the other two.
1011
+ Override either in a `.env.local`.
1012
+
1013
+ The example never signs anyone in: "Connect with Weave" opens the home, where
1014
+ you make an account or sign in, and allow the example your whole account. Its
1015
+ avatar menu opens the home for account settings.
1016
+
1017
+ To give the node a space, create an invite link in the app and:
1018
+
1019
+ ```bash
1020
+ npm run weave -- spaces join --invite '<link>'
1021
+ npm run weave -- records list --space <id>
1022
+ ```
1023
+
1024
+ Close every browser holding the space, open the link somewhere else, and the
1025
+ records come from the node. Or make the node your own account's — see
1026
+ [cli/README.md](cli/README.md) — and it serves every space you make, unasked.
1027
+
1028
+ Between them they exercise the stack end to end. At the home: choose where
1029
+ your data lives (a pod, or this browser), create an account (a password your
1030
+ password manager keeps) or sign in to one, stay signed in, add a passkey, move
1031
+ between pods, pair a phone by QR code, and see which apps you connected. In the
1032
+ example: make private or public spaces, just yours or with people you invite, and share one with a
1033
+ friend via an invite link. Everyone in a space is shown by the name
1034
+ they gave. Every record is signed by a delegated session key, stored in that
1035
+ space's MST, encrypted first if the space is private, and gossiped to peers over
1036
+ WebRTC; a record says *verified* once its signature and its delegation chain
1037
+ check out here, and *encrypted* when it arrived encrypted.
1038
+
1039
+ **Derived UI.** A space opens on its **Apps** tab: apps built on the standard
1040
+ schemas (a chat, a kanban board) show up once the space holds the collections
1041
+ they need, and adding one defines what is missing. The **Collections** tab
1042
+ lists every collection down the side — the space's catalogue. Each one is a list you can search and add to in one line, or a
1043
+ table, or — when it has a field with fixed choices — a board you drag cards
1044
+ across; a yes/no field becomes a checkbox on each row. A record opens in a
1045
+ panel beside the list: its fields as properties you edit in place, what it
1046
+ points at and what points at it, and reactions, tags and comments, which get a place on every record once the
1047
+ space has added them from the library. And there is a "+ Add …" button for every collection
1048
+ that declares a link to this collection: define `app.poll.vote` with
1049
+ `about → app.poll` and every poll gets "+ Add vote". Choices show by their label: `oneOf: [{ const, title }]`
1050
+ for fixed ones, and `x-choicesFrom: { rel: 'about', field: 'options' }` for a
1051
+ field that picks from a list in the linked record — so a vote stored as `1`
1052
+ shows as "Lisbon", its form offers the poll's options, and the poll shows a
1053
+ tally. The helpers that work this out are pure functions
1054
+ (`example/src/derive/schema-ui.ts`), with nothing DOM-specific in them. An
1055
+ empty space offers a small "define a collection" form; an agent can do the
1056
+ same over WebMCP.
1057
+
1058
+ **Agents in the browser (WebMCP).** When `/app` loads, it registers
1059
+ every node operation as a WebMCP tool on `document.modelContext`
1060
+ (`example/src/webmcp.ts`, with `@mcp-b/webmcp-polyfill`: Chrome's own WebMCP
1061
+ when present, a polyfill otherwise). A browser agent or extension sees the same
1062
+ tools as the CLI and `weave mcp` — `spaces_list`, `records_query`,
1063
+ `records_put`, `apps_propose`… — and works as the person, with nothing to
1064
+ switch on: anything that can call a page's tools can already click through the
1065
+ page, so a key of its own would stop nothing. It isn't offered
1066
+ `collections_define`: it proposes apps (`apps_propose`) and a person adds
1067
+ them. Anything that changes a space's people, or hands out its key, asks the
1068
+ person first. An app may bring its own screen: a collection definition's
1069
+ `screen`, one HTML document, which the example runs in a sandboxed frame with
1070
+ no network, talking to the space only through a message port
1071
+ (`createScreenBridge`, `apps_screen_guide`). `docs/screens/chess.html` is one
1072
+ an agent wrote.
1073
+
1074
+ **Agents on your computer (Claude Code, Claude Desktop, Cursor).** "Connect an
1075
+ agent", in the account menu, shows one command:
1076
+ `npx @weaveprotocol/cli connect wv_…`. The terminal makes its own key, finds
1077
+ the tab through the relay, and the person allows it at their account home,
1078
+ which signs an agent's note for the whole account, for as long as they chose.
1079
+ Everything said on the way is sealed with a key from the code, so the relay
1080
+ learns nothing (`src/session/agent-link.ts`). The command then adds `weave` to
1081
+ the agents it finds, and they start `weave mcp` themselves: a node of its own,
1082
+ over WebRTC (`node-datachannel`), that follows the account and keeps working
1083
+ with every tab closed. What it writes shows "via agent", and every peer
1084
+ ignores an agent changing collections, who may do what, or the account's own
1085
+ list of spaces. See BLOCK-20.
1086
+
1087
+ ## Tests
1088
+
1089
+ ```bash
1090
+ npm test
1091
+ ```
1092
+
1093
+ Covers key derivation — checked against the public keys Web Crypto generates
1094
+ for the same private scalars, and pinned to recorded DIDs so an accidental
1095
+ change cannot slip through; recovery codes; account vaults, wraps and account
1096
+ stores; data folders with several writers; spaces, invites and
1097
+ encrypt-then-sign; UCAN issuing, attenuation and chain validation; phone
1098
+ pairing; peer introductions; the MST; the validation gates; and two peers
1099
+ reconciling over the anti-entropy protocol, including the forged, stolen,
1100
+ unauthorized and malformed expressions their gatekeepers reject. Above those:
1101
+ the node API — versioned records, links, queries, collection definitions,
1102
+ profiles, the account registry, moving and merging accounts — and the CLI.
1103
+
1104
+ ## License
1105
+
1106
+ MIT