@kubun/plugin-p2p 0.10.1 → 0.12.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 (201) hide show
  1. package/lib/context/delegation.js +130 -1
  2. package/lib/context/group.js +1646 -1
  3. package/lib/context/hub.js +151 -1
  4. package/lib/context/join.js +115 -1
  5. package/lib/context/peer.d.ts +92 -0
  6. package/lib/context/peer.js +1216 -0
  7. package/lib/context/require-admin.d.ts +57 -0
  8. package/lib/context/require-admin.js +57 -0
  9. package/lib/context/sync.d.ts +12 -0
  10. package/lib/context/sync.js +252 -1
  11. package/lib/context/types.d.ts +58 -22
  12. package/lib/context/types.js +48 -1
  13. package/lib/groups/access-default-token.d.ts +44 -0
  14. package/lib/groups/access-default-token.js +103 -0
  15. package/lib/groups/admin-roster.d.ts +55 -0
  16. package/lib/groups/admin-roster.js +69 -0
  17. package/lib/groups/anchor-store.d.ts +15 -0
  18. package/lib/groups/anchor-store.js +37 -0
  19. package/lib/groups/app-cursor-store.d.ts +17 -0
  20. package/lib/groups/app-cursor-store.js +34 -0
  21. package/lib/groups/broadcast-codec.js +6 -1
  22. package/lib/groups/broadcast.d.ts +238 -106
  23. package/lib/groups/broadcast.js +702 -1
  24. package/lib/groups/catalog-token.d.ts +50 -0
  25. package/lib/groups/catalog-token.js +96 -0
  26. package/lib/groups/circle-projection.d.ts +90 -0
  27. package/lib/groups/circle-projection.js +202 -0
  28. package/lib/groups/circle-reducers.d.ts +139 -0
  29. package/lib/groups/circle-reducers.js +128 -0
  30. package/lib/groups/commit-adoption.d.ts +77 -0
  31. package/lib/groups/commit-adoption.js +108 -0
  32. package/lib/groups/commit-journal.d.ts +14 -0
  33. package/lib/groups/commit-journal.js +65 -0
  34. package/lib/groups/control-request.d.ts +66 -0
  35. package/lib/groups/control-request.js +101 -0
  36. package/lib/groups/events.d.ts +19 -2
  37. package/lib/groups/events.js +10 -1
  38. package/lib/groups/group-anchor.d.ts +24 -0
  39. package/lib/groups/group-anchor.js +22 -0
  40. package/lib/groups/group-crypto.d.ts +42 -0
  41. package/lib/groups/group-crypto.js +159 -0
  42. package/lib/groups/group-handle-registry.d.ts +138 -9
  43. package/lib/groups/group-handle-registry.js +533 -1
  44. package/lib/groups/group-handlers.d.ts +30 -0
  45. package/lib/groups/group-handlers.js +218 -0
  46. package/lib/groups/group-health-monitor.d.ts +11 -0
  47. package/lib/groups/group-health-monitor.js +139 -1
  48. package/lib/groups/group-mls.d.ts +66 -0
  49. package/lib/groups/group-mls.js +696 -0
  50. package/lib/groups/group-peer-manager.d.ts +301 -0
  51. package/lib/groups/group-peer-manager.js +1336 -0
  52. package/lib/groups/group-protocols.d.ts +754 -0
  53. package/lib/groups/group-protocols.js +448 -0
  54. package/lib/groups/invite-payload.d.ts +49 -1
  55. package/lib/groups/invite-payload.js +26 -1
  56. package/lib/groups/join-request-identity.d.ts +21 -0
  57. package/lib/groups/join-request-identity.js +43 -0
  58. package/lib/groups/join-utils.d.ts +61 -0
  59. package/lib/groups/join-utils.js +366 -1
  60. package/lib/groups/ledger-affected-events.d.ts +20 -0
  61. package/lib/groups/ledger-affected-events.js +85 -0
  62. package/lib/groups/ledger-commit-fold.d.ts +107 -0
  63. package/lib/groups/ledger-commit-fold.js +167 -0
  64. package/lib/groups/ledger-ingest.d.ts +124 -0
  65. package/lib/groups/ledger-ingest.js +212 -0
  66. package/lib/groups/ledger.d.ts +44 -0
  67. package/lib/groups/ledger.js +30 -0
  68. package/lib/groups/manager.d.ts +217 -85
  69. package/lib/groups/manager.js +798 -1
  70. package/lib/groups/mls-codec.d.ts +28 -7
  71. package/lib/groups/mls-codec.js +33 -1
  72. package/lib/groups/mls-encryptor.d.ts +2 -2
  73. package/lib/groups/mls-encryptor.js +33 -1
  74. package/lib/groups/mls-group-handle.d.ts +37 -2
  75. package/lib/groups/mls-group-handle.js +18 -1
  76. package/lib/groups/mls-json.js +17 -1
  77. package/lib/groups/mls-receive-errors.d.ts +27 -0
  78. package/lib/groups/mls-receive-errors.js +39 -0
  79. package/lib/groups/mls-state.d.ts +6 -4
  80. package/lib/groups/mls-state.js +79 -1
  81. package/lib/groups/peer-presence.d.ts +90 -0
  82. package/lib/groups/peer-presence.js +295 -0
  83. package/lib/groups/peer-selection.d.ts +37 -0
  84. package/lib/groups/peer-selection.js +72 -0
  85. package/lib/groups/roster-projection.d.ts +53 -0
  86. package/lib/groups/roster-projection.js +65 -0
  87. package/lib/groups/store-received-grant.d.ts +7 -3
  88. package/lib/groups/store-received-grant.js +161 -1
  89. package/lib/groups/store-received-revocation.d.ts +19 -16
  90. package/lib/groups/store-received-revocation.js +83 -1
  91. package/lib/hub/ack-backoff.d.ts +22 -0
  92. package/lib/hub/ack-backoff.js +88 -0
  93. package/lib/hub/http-client.d.ts +18 -10
  94. package/lib/hub/http-client.js +15 -1
  95. package/lib/hub/hub-like.d.ts +75 -0
  96. package/lib/hub/hub-like.js +1022 -0
  97. package/lib/hub/loopback-log-hub.d.ts +20 -0
  98. package/lib/hub/loopback-log-hub.js +178 -0
  99. package/lib/hub/manager.d.ts +25 -40
  100. package/lib/hub/manager.js +155 -1
  101. package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
  102. package/lib/hub/peer-scoped-hub-view.js +92 -0
  103. package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
  104. package/lib/hub/sender-scoped-hub-view.js +59 -0
  105. package/lib/hub/server-did.d.ts +67 -0
  106. package/lib/hub/server-did.js +131 -0
  107. package/lib/hub/wiring.d.ts +77 -51
  108. package/lib/hub/wiring.js +189 -1
  109. package/lib/index.d.ts +58 -22
  110. package/lib/index.js +563 -1
  111. package/lib/peer/connection-registry.d.ts +45 -0
  112. package/lib/peer/connection-registry.js +52 -0
  113. package/lib/peer/handlers.d.ts +69 -0
  114. package/lib/peer/handlers.js +110 -0
  115. package/lib/protocol.d.ts +214 -1
  116. package/lib/protocol.js +519 -1
  117. package/lib/schema.d.ts +3 -1
  118. package/lib/schema.js +1150 -39
  119. package/lib/sync/access-default-sender.d.ts +23 -0
  120. package/lib/sync/access-default-sender.js +80 -0
  121. package/lib/sync/authorize.d.ts +58 -4
  122. package/lib/sync/authorize.js +114 -1
  123. package/lib/sync/broadcast-queue.d.ts +3 -4
  124. package/lib/sync/broadcast-queue.js +182 -1
  125. package/lib/sync/broadcast-sender.js +179 -1
  126. package/lib/sync/catalog-scope.d.ts +8 -7
  127. package/lib/sync/catalog-scope.js +84 -1
  128. package/lib/sync/errors.d.ts +12 -2
  129. package/lib/sync/errors.js +29 -1
  130. package/lib/sync/forwarder.d.ts +13 -19
  131. package/lib/sync/forwarder.js +271 -1
  132. package/lib/sync/handlers.js +200 -1
  133. package/lib/sync/hub-tunnel-sync-listener.d.ts +19 -5
  134. package/lib/sync/hub-tunnel-sync-listener.js +261 -1
  135. package/lib/sync/hub-tunnel-sync-provider.d.ts +34 -14
  136. package/lib/sync/hub-tunnel-sync-provider.js +113 -1
  137. package/lib/sync/merkle-apply.d.ts +5 -0
  138. package/lib/sync/merkle-apply.js +194 -1
  139. package/lib/sync/merkle-channel.d.ts +11 -3
  140. package/lib/sync/merkle-channel.js +0 -0
  141. package/lib/sync/merkle-tree.js +120 -1
  142. package/lib/sync/peer-registry.d.ts +15 -11
  143. package/lib/sync/peer-registry.js +82 -1
  144. package/lib/sync/receive-access-gate.js +24 -1
  145. package/lib/sync/scope-resolver.js +41 -1
  146. package/lib/sync/sync-client.d.ts +14 -4
  147. package/lib/sync/sync-client.js +254 -1
  148. package/lib/sync/sync-manager.d.ts +24 -6
  149. package/lib/sync/sync-manager.js +301 -1
  150. package/lib/sync/tunnel-listeners.d.ts +52 -0
  151. package/lib/sync/tunnel-listeners.js +175 -0
  152. package/lib/sync/tunnel-observability.d.ts +25 -0
  153. package/lib/sync/tunnel-observability.js +63 -0
  154. package/lib/sync/tunnel-topics.d.ts +20 -0
  155. package/lib/sync/tunnel-topics.js +28 -0
  156. package/lib/types.d.ts +704 -22
  157. package/lib/types.js +1 -1
  158. package/lib/util/handler-error.d.ts +16 -0
  159. package/lib/util/handler-error.js +64 -0
  160. package/lib/util/mutex.d.ts +14 -1
  161. package/lib/util/mutex.js +44 -1
  162. package/lib/utils.js +21 -1
  163. package/package.json +58 -47
  164. package/lib/groups/broadcast-service.d.ts +0 -263
  165. package/lib/groups/broadcast-service.js +0 -1
  166. package/lib/groups/rejoin-codec.d.ts +0 -14
  167. package/lib/groups/rejoin-codec.js +0 -1
  168. package/lib/groups/wire-frame.d.ts +0 -34
  169. package/lib/groups/wire-frame.js +0 -1
  170. package/lib/hub/circle-catchup-requester.d.ts +0 -58
  171. package/lib/hub/circle-catchup-requester.js +0 -1
  172. package/lib/hub/circle-catchup-responder.d.ts +0 -50
  173. package/lib/hub/circle-catchup-responder.js +0 -1
  174. package/lib/hub/connection-pool.d.ts +0 -43
  175. package/lib/hub/connection-pool.js +0 -1
  176. package/lib/hub/did-observing-transport.d.ts +0 -64
  177. package/lib/hub/did-observing-transport.js +0 -1
  178. package/lib/hub/epoch-stale-detector.d.ts +0 -18
  179. package/lib/hub/epoch-stale-detector.js +0 -1
  180. package/lib/hub/errors.d.ts +0 -30
  181. package/lib/hub/errors.js +0 -1
  182. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  183. package/lib/hub/forward-remote-broadcast.js +0 -1
  184. package/lib/hub/group-channel.d.ts +0 -89
  185. package/lib/hub/group-channel.js +0 -1
  186. package/lib/hub/hub-connection.d.ts +0 -96
  187. package/lib/hub/hub-connection.js +0 -1
  188. package/lib/hub/receive-handler.d.ts +0 -51
  189. package/lib/hub/receive-handler.js +0 -1
  190. package/lib/hub/rejoin-manager.d.ts +0 -78
  191. package/lib/hub/rejoin-manager.js +0 -1
  192. package/lib/hub/rejoin-responder.d.ts +0 -32
  193. package/lib/hub/rejoin-responder.js +0 -1
  194. package/lib/hub/relay-manager.d.ts +0 -142
  195. package/lib/hub/relay-manager.js +0 -1
  196. package/lib/hub/send-handler.d.ts +0 -40
  197. package/lib/hub/send-handler.js +0 -1
  198. package/lib/hub/tunnel-inbox.d.ts +0 -20
  199. package/lib/hub/tunnel-inbox.js +0 -1
  200. package/lib/hub/wait-for-gate.d.ts +0 -14
  201. package/lib/hub/wait-for-gate.js +0 -1
package/lib/types.js CHANGED
@@ -1 +1 @@
1
- export{};
1
+ export { };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Translates a thrown value into a `HandlerError` when it carries a registered
3
+ * `KubunErrorCode`, so `HandlerError.from` in `@enkaku/server` passes it through
4
+ * instead of flattening it to `EK01` / "Handler execution failed".
5
+ *
6
+ * Anything else is returned unchanged, and that default is the security
7
+ * property: an error with no registered code is internal, and its message may
8
+ * carry DIDs, paths, or SQL. Forwarding a refusal is safe only because the
9
+ * throw site attached a code the registry allows to leave the device.
10
+ */
11
+ export declare function toHandlerError(cause: unknown): unknown;
12
+ /**
13
+ * Applies {@link toHandlerError} to every handler in a procedure record, so the
14
+ * translation lives in one place rather than in each handler.
15
+ */
16
+ export declare function wrapHandlers(handlers: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,64 @@
1
+ import { HandlerError } from '@enkaku/server';
2
+ import { isKubunErrorCode } from '@kubun/protocol';
3
+ import { GraphQLError } from 'graphql';
4
+ function isObject(value) {
5
+ return typeof value === 'object' && value != null && !Array.isArray(value);
6
+ }
7
+ /**
8
+ * Translates a thrown value into a `HandlerError` when it carries a registered
9
+ * `KubunErrorCode`, so `HandlerError.from` in `@enkaku/server` passes it through
10
+ * instead of flattening it to `EK01` / "Handler execution failed".
11
+ *
12
+ * Anything else is returned unchanged, and that default is the security
13
+ * property: an error with no registered code is internal, and its message may
14
+ * carry DIDs, paths, or SQL. Forwarding a refusal is safe only because the
15
+ * throw site attached a code the registry allows to leave the device.
16
+ */ export function toHandlerError(cause) {
17
+ if (cause instanceof HandlerError) {
18
+ return cause;
19
+ }
20
+ if (cause instanceof GraphQLError) {
21
+ const { code, ...data } = cause.extensions;
22
+ return isKubunErrorCode(code) ? new HandlerError({
23
+ code,
24
+ message: cause.message,
25
+ data,
26
+ cause
27
+ }) : cause;
28
+ }
29
+ if (isObject(cause) && isKubunErrorCode(cause.code)) {
30
+ const message = typeof cause.message === 'string' ? cause.message : undefined;
31
+ const data = isObject(cause.data) ? cause.data : {};
32
+ return new HandlerError({
33
+ code: cause.code,
34
+ message,
35
+ data,
36
+ cause
37
+ });
38
+ }
39
+ return cause;
40
+ }
41
+ /**
42
+ * Applies {@link toHandlerError} to every handler in a procedure record, so the
43
+ * translation lives in one place rather than in each handler.
44
+ */ export function wrapHandlers(handlers) {
45
+ return Object.fromEntries(Object.entries(handlers).map(([name, handler])=>{
46
+ if (typeof handler !== 'function') {
47
+ return [
48
+ name,
49
+ handler
50
+ ];
51
+ }
52
+ const wrapped = async (...args)=>{
53
+ try {
54
+ return await handler(...args);
55
+ } catch (cause) {
56
+ throw toHandlerError(cause);
57
+ }
58
+ };
59
+ return [
60
+ name,
61
+ wrapped
62
+ ];
63
+ }));
64
+ }
@@ -1,4 +1,17 @@
1
+ export type MutexRunOptions = {
2
+ /**
3
+ * Give up WAITING for the lock after this long. Bounds the wait only — the
4
+ * deadline is cleared when the callback is invoked, so a slow holder is never
5
+ * rejected out from under work that then completes.
6
+ *
7
+ * A backstop, not a policy. A waiter that gives up does NOT run its callback
8
+ * when its turn comes; it yields the slot so everything behind it proceeds.
9
+ */
10
+ acquireTimeoutMs?: number;
11
+ /** Builds the rejection, so the caller can name what it was waiting for. */
12
+ acquireTimeoutError?: () => Error;
13
+ };
1
14
  export type Mutex = {
2
- run<T>(fn: () => Promise<T>): Promise<T>;
15
+ run<T>(fn: () => Promise<T>, options?: MutexRunOptions): Promise<T>;
3
16
  };
4
17
  export declare function createMutex(): Mutex;
package/lib/util/mutex.js CHANGED
@@ -1 +1,44 @@
1
- export function createMutex(){let e=Promise.resolve();return{run(t){let r=e.then(()=>t());return e=r.catch(()=>{}),r}}}
1
+ /** A waiter that gave up before its turn. Read by nothing: its `run` already rejected. */ const EXPIRED = Symbol('mutex.expired');
2
+ export function createMutex() {
3
+ let tail = Promise.resolve();
4
+ return {
5
+ run (fn, options) {
6
+ const timeoutMs = options?.acquireTimeoutMs;
7
+ let expired = false;
8
+ let timer;
9
+ // The race below is on the RESULT, never on the acquisition: racing the
10
+ // acquisition inserts microtask hops between a handle operation and
11
+ // everything ordered against it. `fn` stays one microtask off the
12
+ // previous holder.
13
+ const next = tail.then(async ()=>{
14
+ if (expired) {
15
+ return EXPIRED;
16
+ }
17
+ // Safe to read a timer the executor below assigns: `new Promise` runs
18
+ // its executor synchronously, this callback is a microtask away.
19
+ if (timer != null) {
20
+ clearTimeout(timer);
21
+ timer = undefined;
22
+ }
23
+ return await fn();
24
+ });
25
+ tail = next.catch(()=>{});
26
+ if (timeoutMs == null) {
27
+ return next;
28
+ }
29
+ return new Promise((resolve, reject)=>{
30
+ timer = setTimeout(()=>{
31
+ expired = true;
32
+ reject(options?.acquireTimeoutError?.() ?? new Error(`lock not acquired in ${timeoutMs}ms`));
33
+ }, timeoutMs);
34
+ next.then((value)=>{
35
+ clearTimeout(timer);
36
+ resolve(value);
37
+ }, (error)=>{
38
+ clearTimeout(timer);
39
+ reject(error);
40
+ });
41
+ });
42
+ }
43
+ };
44
+ }
package/lib/utils.js CHANGED
@@ -1 +1,21 @@
1
- export function unref(e){e.unref?.()}export function sleep(e){return new Promise(n=>{unref(setTimeout(n,e))})}export function bytesEqual(e,n){if(e.length!==n.length)return!1;for(let t=0;t<e.length;t++)if(e[t]!==n[t])return!1;return!0}
1
+ /** Node returns a Timeout object with `unref()`; browsers return a number. */ export function unref(timer) {
2
+ ;
3
+ timer.unref?.();
4
+ }
5
+ /** Resolve after `ms`, without keeping the process alive. */ export function sleep(ms) {
6
+ return new Promise((resolve)=>{
7
+ const timer = setTimeout(resolve, ms);
8
+ unref(timer);
9
+ });
10
+ }
11
+ export function bytesEqual(a, b) {
12
+ if (a.length !== b.length) {
13
+ return false;
14
+ }
15
+ for(let i = 0; i < a.length; i++){
16
+ if (a[i] !== b[i]) {
17
+ return false;
18
+ }
19
+ }
20
+ return true;
21
+ }
package/package.json CHANGED
@@ -1,71 +1,82 @@
1
1
  {
2
2
  "name": "@kubun/plugin-p2p",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "license": "see LICENSE.md",
5
+ "sideEffects": false,
5
6
  "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
7
  "exports": {
9
8
  ".": "./lib/index.js"
10
9
  },
10
+ "main": "lib/index.js",
11
+ "types": "lib/index.d.ts",
11
12
  "files": [
12
13
  "lib/*",
13
14
  "LICENSE.md"
14
15
  ],
15
- "sideEffects": false,
16
16
  "dependencies": {
17
- "@enkaku/async": "^0.16.0",
18
- "@enkaku/capability": "^0.16.0",
19
- "@enkaku/client": "^0.16.0",
20
- "@enkaku/codec": "^0.16.0",
21
- "@enkaku/event": "^0.16.0",
22
- "@enkaku/generator": "^0.16.0",
23
- "@enkaku/group": "^0.16.2",
24
- "@enkaku/http-client-transport": "^0.16.0",
25
- "@enkaku/http-server-transport": "^0.16.0",
26
- "@enkaku/hub-protocol": "^0.16.0",
27
- "@enkaku/hub-tunnel": "^0.16.0",
28
- "@enkaku/protocol": "^0.16.0",
29
- "@enkaku/runtime": "^0.16.0",
30
- "@enkaku/schema": "^0.16.0",
31
- "@enkaku/server": "^0.16.0",
32
- "@enkaku/stream": "^0.16.0",
33
- "@enkaku/token": "^0.16.0",
34
- "@enkaku/transport": "^0.16.0",
17
+ "@enkaku/client": "^0.20.0",
18
+ "@enkaku/http-fetch": "^0.20.0",
19
+ "@enkaku/http-serve": "^0.20.0",
20
+ "@enkaku/protocol": "^0.20.0",
21
+ "@enkaku/server": "^0.20.0",
22
+ "@enkaku/transport": "^0.20.0",
23
+ "@kokuin/capability": "^0.2.1",
24
+ "@kokuin/token": "^0.3.0",
25
+ "@kumiai/broadcast": "^0.5.0",
26
+ "@kumiai/hub-protocol": "^0.5.0",
27
+ "@kumiai/hub-server": "^0.5.0",
28
+ "@kumiai/hub-tunnel": "^0.5.0",
29
+ "@kumiai/mls": "^0.5.0",
30
+ "@kumiai/mls-rpc": "^0.5.0",
31
+ "@kumiai/rpc": "^0.5.0",
32
+ "@noble/ciphers": "^2.2.0",
35
33
  "@noble/hashes": "^2.2.0",
34
+ "@sozai/async": "^0.2.1",
35
+ "@sozai/codec": "^0.4.0",
36
+ "@sozai/event": "^0.1.3",
37
+ "@sozai/generator": "^0.2.0",
38
+ "@sozai/runtime": "^0.1.0",
39
+ "@sozai/schema": "^0.1.1",
40
+ "@sozai/stream": "^0.2.0",
36
41
  "graphql": "^16.14.2",
37
- "kysely": "^0.29.2",
38
- "@kubun/client": "^0.10.1",
39
- "@kubun/db-adapter": "^0.10.0",
40
- "@kubun/db": "^0.10.0",
41
- "@kubun/engine": "^0.10.3",
42
- "@kubun/store-graph": "^0.10.2",
43
- "@kubun/store-p2p": "^0.10.1",
44
- "@kubun/hlc": "^0.10.1",
45
- "@kubun/graphql": "^0.10.1",
46
- "@kubun/id": "^0.10.0",
47
- "@kubun/mutation": "^0.10.1",
48
- "@kubun/logger": "^0.10.0",
49
- "@kubun/protocol": "^0.10.0"
42
+ "kysely": "^0.29.4",
43
+ "ts-mls": "2.0.0-rc.13",
44
+ "@kubun/db": "^0.12.0",
45
+ "@kubun/db-adapter": "^0.12.0",
46
+ "@kubun/engine": "^0.12.0",
47
+ "@kubun/graphql": "^0.12.0",
48
+ "@kubun/hlc": "^0.12.0",
49
+ "@kubun/http-util": "^0.12.0",
50
+ "@kubun/logger": "^0.12.0",
51
+ "@kubun/mutation": "^0.12.0",
52
+ "@kubun/plugin-http": "^0.12.0",
53
+ "@kubun/plugin-rpc": "^0.12.0",
54
+ "@kubun/protocol": "^0.12.0",
55
+ "@kubun/store-delegation": "^0.12.0",
56
+ "@kubun/id": "^0.12.0",
57
+ "@kubun/store-graph": "^0.12.0",
58
+ "@kubun/store-p2p": "^0.12.0"
50
59
  },
51
60
  "devDependencies": {
52
- "@enkaku/hub-server": "^0.16.0",
61
+ "@kumiai/hub-conformance": "^0.5.0",
62
+ "@kumiai/rpc-conformance": "^0.5.0",
63
+ "@testcontainers/postgresql": "^12.1.0",
53
64
  "get-port": "^7.2.0",
54
- "@kubun/db-better-sqlite": "^0.10.0",
55
- "@kubun/hub": "^0.10.0",
56
- "@kubun/plugin-connector": "^0.10.0",
57
- "@kubun/db-node-sqlite": "^0.10.0",
58
- "@kubun/plugin-http": "^0.10.0",
59
- "@kubun/plugin-rpc": "^0.10.0",
60
- "@kubun/test-utils": "^0.10.0"
65
+ "@kubun/db-node-sqlite": "^0.12.0",
66
+ "@kubun/client": "^0.12.0",
67
+ "@kubun/plugin-connector": "^0.12.0",
68
+ "@kubun/test-utils": "^0.12.0",
69
+ "@kubun/db-postgres": "^0.12.0",
70
+ "@kubun/hub": "^0.12.0",
71
+ "@kubun/db-better-sqlite": "^0.12.0"
61
72
  },
62
73
  "scripts": {
74
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
63
75
  "build:clean": "del lib",
64
- "build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
76
+ "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
65
77
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
66
- "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
78
+ "test": "pnpm run test:types && pnpm run test:unit",
67
79
  "test:types": "tsc --noEmit -p tsconfig.test.json",
68
- "test:unit": "vitest run",
69
- "test": "pnpm run test:types && pnpm run test:unit"
80
+ "test:unit": "vitest run"
70
81
  }
71
82
  }
@@ -1,263 +0,0 @@
1
- import { EventEmitter } from '@enkaku/event';
2
- import { type GroupHandle, type MemberCredential } from '@enkaku/group';
3
- import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
4
- import { HLC } from '@kubun/hlc';
5
- import type { Logger } from '@kubun/logger';
6
- import type { GraphStoreAPI } from '@kubun/store-graph';
7
- import type { P2PStoreAPI } from '@kubun/store-p2p';
8
- import type { ForwardingConfig } from '../sync/forwarder.js';
9
- import type { StoreUnreadableMode } from '../types.js';
10
- import { type AffectedRow, type ApplyCatchupReplyResult, type GroupBroadcastMessage } from './broadcast.js';
11
- import type { P2PEventEmitter } from './events.js';
12
- import type { GroupHandleRegistry } from './group-handle-registry.js';
13
- /**
14
- * Thrown when a received broadcast fails to decrypt (MLS restore, decrypt,
15
- * or deserialize). Callers should typically ack-and-skip these, since the
16
- * message is unrecoverable and redelivery will not help.
17
- */
18
- export declare class DecryptError extends Error {
19
- #private;
20
- constructor(message: string, cause: unknown);
21
- get cause(): unknown;
22
- }
23
- /**
24
- * Decode failure on a rejoin control frame. Distinct from {@link DecryptError}
25
- * so the receive path classifies it `control`, not `decrypt-failed` — a corrupt
26
- * frame is not evidence of epoch-staleness and must not drive the health
27
- * detector. Ack-and-skip: redelivery can't fix a malformed frame.
28
- */
29
- export declare class MalformedControlError extends Error {
30
- #private;
31
- constructor(message: string, cause: unknown);
32
- get cause(): unknown;
33
- }
34
- /**
35
- * Thrown when a successfully decrypted broadcast fails to apply to the local
36
- * DB. Callers should typically NOT ack these, so the message can be
37
- * redelivered after the transient failure clears.
38
- */
39
- export declare class ApplyError extends Error {
40
- #private;
41
- constructor(message: string, cause: unknown);
42
- get cause(): unknown;
43
- }
44
- /**
45
- * Thrown when a received MLS Commit targets a future epoch the local handle
46
- * has not reached yet (a gap in the in-order Commit stream). Callers should
47
- * NOT ack — the hub redelivers in `sequence_id` order, so the gap-filling
48
- * Commit arrives first and this one applies on a later delivery.
49
- */
50
- export declare class CommitOutOfOrderError extends Error {
51
- #private;
52
- constructor(messageEpoch: bigint, handleEpoch: bigint);
53
- get messageEpoch(): bigint;
54
- get handleEpoch(): bigint;
55
- }
56
- export type BroadcastEvent = {
57
- groupID: string;
58
- message: GroupBroadcastMessage;
59
- applied: boolean;
60
- /**
61
- * Row affected by the broadcast. After-state for create / update,
62
- * before-state for delete / remove. Undefined when `applied === false`.
63
- */
64
- affected?: AffectedRow;
65
- /**
66
- * Count of `mutation:apply` entries the local broadcast log already had.
67
- * Only set on the `mutation:apply` branch; absent on control-plane
68
- * variants.
69
- */
70
- duplicates?: number;
71
- };
72
- export type BroadcastServiceEvents = {
73
- broadcast: BroadcastEvent;
74
- };
75
- export type SendBroadcastParams = {
76
- groupID: string;
77
- message: GroupBroadcastMessage;
78
- /**
79
- * Wire-send hook invoked with the MLS-encrypted payload INSIDE the
80
- * registry's per-group lock. A throw here propagates out of `prepareSend`
81
- * and the registry skips persist — so a failed send leaves the device's
82
- * MLS state at its pre-encrypt epoch, preventing the wedge where local
83
- * state outpaces recipients.
84
- */
85
- send: (encrypted: Uint8Array) => Promise<void>;
86
- };
87
- export type SendCommitParams = {
88
- groupID: string;
89
- /**
90
- * Canonical MLS-encoded handshake Commit bytes (an encrypted
91
- * `PrivateMessage`) produced by the committer's `GroupManager` call.
92
- */
93
- commitBytes: Uint8Array;
94
- /** Wire-send hook; receives the `mls`-framed bytes. */
95
- send: (framed: Uint8Array) => Promise<void>;
96
- };
97
- export type ReceiveBroadcastParams = {
98
- groupID: string;
99
- encrypted: Uint8Array;
100
- };
101
- export type ReceiveAppResult = {
102
- kind: 'app';
103
- message: GroupBroadcastMessage;
104
- applied: boolean;
105
- affected?: AffectedRow;
106
- duplicates?: number;
107
- };
108
- /**
109
- * Result of processing a received MLS handshake Commit (`mls` wire frame).
110
- * `removed`/`added` are the membership deltas derived by diffing the group
111
- * roster before and after applying the Commit. `removed` DIDs are tombstoned
112
- * via `markMemberRemoved` so the apply-time membership check can reject their
113
- * post-removal writes.
114
- */
115
- export type ReceiveCommitResult = {
116
- kind: 'mls';
117
- epoch: bigint;
118
- removed: Array<string>;
119
- added: Array<string>;
120
- };
121
- /**
122
- * A received `rejoin-request`: a stale device asking current members for fresh
123
- * GroupInfo. Cleartext; does not touch the MLS handle.
124
- */
125
- export type ReceiveRejoinRequestResult = {
126
- kind: 'rejoin-request';
127
- requestID: string;
128
- };
129
- /**
130
- * A current member's `rejoin-groupinfo` reply carrying fresh GroupInfo. Consumed
131
- * by the requester's rejoin flow; also drives responder suppression on peers.
132
- */
133
- export type ReceiveRejoinGroupInfoResult = {
134
- kind: 'rejoin-groupinfo';
135
- requestID: string;
136
- groupInfo: Uint8Array;
137
- };
138
- export type ReceiveBroadcastResult = ReceiveAppResult | ReceiveCommitResult | ReceiveRejoinRequestResult | ReceiveRejoinGroupInfoResult;
139
- export type SendRejoinRequestParams = {
140
- requestID: string;
141
- /** Wire-send hook; receives the `rejoinRequest`-framed cleartext bytes. */
142
- send: (framed: Uint8Array) => Promise<void>;
143
- };
144
- export type SendRejoinGroupInfoParams = {
145
- requestID: string;
146
- groupInfo: Uint8Array;
147
- /** Wire-send hook; receives the `rejoinGroupInfo`-framed cleartext bytes. */
148
- send: (framed: Uint8Array) => Promise<void>;
149
- };
150
- /**
151
- * BroadcastService handles MLS encrypt/decrypt for group broadcasts
152
- * and applies received messages to the local DB.
153
- *
154
- * Encrypt and decrypt go through `GroupHandleRegistry.withHandle` so MLS
155
- * state mutations serialize through the per-group mutex with write-through
156
- * persist. The DB-apply step (`processBroadcast`) for receives runs OUTSIDE
157
- * the lock so a slow apply does not block subsequent encrypt/decrypt on the
158
- * same group.
159
- */
160
- export type BroadcastServiceParams = {
161
- p2pStore: P2PStoreAPI;
162
- graphStore: GraphStoreAPI;
163
- /**
164
- * Engine graph internals — required so `mutation:apply` broadcasts route
165
- * through {@link GraphInternals.applyVerifiedMutation} with `origin: 'peer'`.
166
- */
167
- graph: GraphInternals;
168
- /** Self DID for `mutation:apply` loopback detection. */
169
- selfDID: string;
170
- /** Single canonical access point for MLS `GroupHandle` instances. */
171
- registry: GroupHandleRegistry;
172
- /**
173
- * Device-wide monotonic clock for stamping the `removed_at_hlc` of members
174
- * dropped by a received MLS Commit (the receive side has no HLC otherwise).
175
- * Required: production passes the engine's single instance so every write
176
- * from this device advances one shared clock, preventing two same-millisecond
177
- * writes from minting identical timestamps (the second would silently lose
178
- * under last-writer-wins).
179
- */
180
- hlc: HLC;
181
- /**
182
- * Receiver storage mode for incoming `mutation:apply` entries. Defaults
183
- * to `'persist'`. When `'drop'`, `defaultAccessLevel` must be supplied
184
- * so the gate factory can resolve server-side default access rules.
185
- */
186
- storeUnreadable?: StoreUnreadableMode;
187
- /** Server default access level — required when `storeUnreadable === 'drop'`. */
188
- defaultAccessLevel?: DefaultAccessLevel;
189
- /**
190
- * Forwarding config. Threaded into `processBroadcast` per call.
191
- * `false` / undefined → forwarding disabled (default).
192
- */
193
- forwarding?: ForwardingConfig;
194
- /**
195
- * Hub forwarding hook — required when `forwarding` is set. Called once per
196
- * surviving candidate group on the receive path.
197
- */
198
- scheduleBroadcast?: (groupID: string, message: GroupBroadcastMessage) => void | Promise<void>;
199
- /**
200
- * Emitter forwarded to `processBroadcast` so `delegation:share` receives
201
- * surface a `delegationTokenAdded` event to local subscribers.
202
- */
203
- emitter?: P2PEventEmitter;
204
- logger?: Logger;
205
- };
206
- export declare class BroadcastService extends EventEmitter<BroadcastServiceEvents> {
207
- #private;
208
- constructor(params: BroadcastServiceParams);
209
- /**
210
- * Encrypt + send a broadcast for a group, then persist the advanced MLS state.
211
- *
212
- * Failure semantics: send happens inside the registry's per-group lock. A
213
- * throw from `send` propagates out and the registry skips persist — so the
214
- * device's MLS state stays at its pre-encrypt epoch and the next attempt
215
- * re-derives a fresh handle from the prior generation. Wedge protection
216
- * intact.
217
- */
218
- prepareSend(params: SendBroadcastParams): Promise<void>;
219
- /**
220
- * Send a pre-produced MLS handshake Commit to the group, tagged `mls` on the
221
- * wire. The Commit is already a self-protected (encrypted `PrivateMessage`)
222
- * framed message produced by the committer's `GroupManager` call — it is NOT
223
- * re-encrypted through the application path, so this does not touch the
224
- * handle. The committer's MLS state has already advanced + persisted; the
225
- * hub's durable + ordered mailbox carries the Commit to existing members and
226
- * replays it on reconnect.
227
- */
228
- prepareSendCommit(params: SendCommitParams): Promise<void>;
229
- /**
230
- * Export a fresh `MLSMessage(GroupInfo)` from the current handle. Read-only:
231
- * takes the registry lock but does not advance or persist state.
232
- */
233
- exportGroupInfo(groupID: string): Promise<Uint8Array>;
234
- readCredential(groupID: string): Promise<MemberCredential>;
235
- /**
236
- * Swap+persist the cached handle (under the per-group mutex), then reconcile
237
- * the store roster. The swap is security-critical; a reconcile failure is
238
- * logged and swallowed so it can't reject the rejoin after the handle is
239
- * installed (which would skip `markRecovered` and wedge the monitor) — the
240
- * next rejoin re-runs the idempotent reconcile.
241
- */
242
- replaceHandle(groupID: string, handle: GroupHandle): Promise<void>;
243
- /** Send a `rejoin-request` (cleartext) to the group channel. */
244
- prepareSendRejoinRequest(params: SendRejoinRequestParams): Promise<void>;
245
- /** Send a `rejoin-groupinfo` reply (cleartext) to the group channel. */
246
- prepareSendRejoinGroupInfo(params: SendRejoinGroupInfoParams): Promise<void>;
247
- /**
248
- * Process a received broadcast: MLS decrypt INSIDE the registry lock,
249
- * then apply to the DB OUTSIDE the lock so a slow apply doesn't block
250
- * subsequent encrypt/decrypt on the same group.
251
- */
252
- processReceived(params: ReceiveBroadcastParams): Promise<ReceiveBroadcastResult>;
253
- /**
254
- * Apply a `circle-catchup:reply` batch (circles + circle members, including
255
- * tombstoned) row-by-row through the existing handlers. Lock-free: like
256
- * `processReceived`'s post-decrypt apply, this runs OUTSIDE the registry's
257
- * per-group lock — it touches no MLS handle, and each row apply is idempotent
258
- * and order-independent, so a late or duplicate reply converges harmlessly.
259
- */
260
- applyCatchupReply(groupID: string, reply: Extract<GroupBroadcastMessage, {
261
- type: 'circle-catchup:reply';
262
- }>): Promise<ApplyCatchupReplyResult>;
263
- }
@@ -1 +0,0 @@
1
- import{EventEmitter as r}from"@enkaku/event";import{exportGroupInfo as e,readMessageEpoch as t}from"@enkaku/group";import{HLC as a}from"@kubun/hlc";import{applyCatchupReply as o,processBroadcast as s}from"./broadcast.js";import{deserializeBroadcast as i,serializeBroadcast as c}from"./broadcast-codec.js";import{mlsDecryptFramed as n,mlsEncryptFramed as l}from"./mls-codec.js";import{decodeRejoinGroupInfo as d,decodeRejoinRequest as m,encodeRejoinGroupInfo as p,encodeRejoinRequest as h}from"./rejoin-codec.js";import{frameWire as u,unframeWire as w,WireKind as f}from"./wire-frame.js";export class DecryptError extends Error{#r;constructor(r,e){super(r),this.name="DecryptError",this.#r=e}get cause(){return this.#r}}export class MalformedControlError extends Error{#r;constructor(r,e){super(r),this.name="MalformedControlError",this.#r=e}get cause(){return this.#r}}export class ApplyError extends Error{#r;constructor(r,e){super(r),this.name="ApplyError",this.#r=e}get cause(){return this.#r}}export class CommitOutOfOrderError extends Error{#e;#t;constructor(r,e){super(`MLS commit epoch ${r} ahead of handle epoch ${e}`),this.name="CommitOutOfOrderError",this.#e=r,this.#t=e}get messageEpoch(){return this.#e}get handleEpoch(){return this.#t}}export class BroadcastService extends r{#a;#o;#s;constructor(r){super(),this.#s=r.hlc,this.#a={p2pStore:r.p2pStore,graphStore:r.graphStore,graph:r.graph,selfDID:r.selfDID,storeUnreadable:r.storeUnreadable,defaultAccessLevel:r.defaultAccessLevel,forwarding:r.forwarding,scheduleBroadcast:r.scheduleBroadcast,emitter:r.emitter,logger:r.logger},this.#o=r.registry}async prepareSend(r){let{groupID:e,message:t,send:a}=r,o=c(t);await this.#o.withHandle(e,async r=>{let{framed:e}=await l(r,o);await a(u(f.app,e))})}async prepareSendCommit(r){let{commitBytes:e,send:t}=r;await t(u(f.mls,e))}async exportGroupInfo(r){return await this.#o.withHandle(r,async r=>{let{groupInfo:t}=await e({group:r});return t})}async readCredential(r){return await this.#o.withHandle(r,async r=>r.credential)}async replaceHandle(r,e){await this.#o.replaceHandle(r,e);try{await this.#i(r,e)}catch(e){this.#a.logger?.warn("rejoin: store roster reconcile failed",{groupID:r,error:e})}}async #i(r,e){let{p2pStore:t}=this.#a,o=e.listMembers().map(r=>r.id),s=new Set(o),i=await t.listGroupMembers(r),c=new Set(i.map(r=>r.member_did)),n=a.serialize(this.#s.now());for(let e of i)s.has(e.member_did)||await t.markMemberRemoved(r,e.member_did,n);for(let e of o)c.has(e)||await t.addGroupMember({group_id:r,member_did:e,role:"member",hlc:n})}async prepareSendRejoinRequest(r){let{requestID:e,send:t}=r;await t(u(f.rejoinRequest,h({requestID:e})))}async prepareSendRejoinGroupInfo(r){let{requestID:e,groupInfo:t,send:a}=r;await a(u(f.rejoinGroupInfo,p({requestID:e,groupInfo:t})))}async processReceived(r){let e,t,a,o,{groupID:c,encrypted:l}=r;try{({kind:e,payload:t}=w(l))}catch(r){throw new DecryptError("Malformed group wire frame",r)}if(e===f.mls)return await this.#c(c,t);if(e===f.rejoinRequest)try{let{requestID:r}=m(t);return{kind:"rejoin-request",requestID:r}}catch(r){throw new MalformedControlError("Malformed rejoin-request frame",r)}if(e===f.rejoinGroupInfo)try{let{requestID:r,groupInfo:e}=d(t);return{kind:"rejoin-groupinfo",requestID:r,groupInfo:e}}catch(r){throw new MalformedControlError("Malformed rejoin-groupinfo frame",r)}try{a=await this.#o.withHandle(c,async r=>{let e=await n(r,t);return i(e)})}catch(r){throw new DecryptError("Failed to decrypt broadcast",r)}try{o=await s(this.#a,a,c)}catch(r){throw new ApplyError("Failed to apply broadcast",r)}let{applied:p,affected:h,duplicates:u}=o;return await this.emit("broadcast",{groupID:c,message:a,applied:p,affected:h,duplicates:u}),{kind:"app",message:a,applied:p,affected:h,duplicates:u}}async applyCatchupReply(r,e){return await o(this.#a,e,r)}async #c(r,e){return await this.#o.withHandle(r,async o=>{let s=t(e),i=o.epoch;if(null!=s&&s<i)return{kind:"mls",epoch:i,removed:[],added:[]};if(null!=s&&s>i)throw new CommitOutOfOrderError(s,i);let c=o.listMembers();try{await o.processMessage(e)}catch(r){throw new DecryptError("Failed to process MLS commit",r)}let n=o.listMembers(),l=new Set(c.map(r=>r.id)),d=new Set(n.map(r=>r.id)),m=c.filter(r=>!d.has(r.id)).map(r=>r.id),p=n.filter(r=>!l.has(r.id)).map(r=>r.id),h=a.serialize(this.#s.now()),{p2pStore:u}=this.#a;for(let e of m)await u.markMemberRemoved(r,e,h);for(let e of p)await u.addGroupMember({group_id:r,member_did:e,role:"member",hlc:h});return{kind:"mls",epoch:o.epoch,removed:m,added:p}})}}
@@ -1,14 +0,0 @@
1
- /** A stale device's request for a fresh GroupInfo. Cleartext on the wire. */
2
- export type RejoinRequest = {
3
- requestID: string;
4
- };
5
- /** A current member's reply carrying a framed `MLSMessage(GroupInfo)`. */
6
- export type RejoinGroupInfo = {
7
- requestID: string;
8
- /** Framed `MLSMessage(GroupInfo)` bytes. */
9
- groupInfo: Uint8Array;
10
- };
11
- export declare function encodeRejoinRequest(request: RejoinRequest): Uint8Array;
12
- export declare function decodeRejoinRequest(bytes: Uint8Array): RejoinRequest;
13
- export declare function encodeRejoinGroupInfo(message: RejoinGroupInfo): Uint8Array;
14
- export declare function decodeRejoinGroupInfo(bytes: Uint8Array): RejoinGroupInfo;
@@ -1 +0,0 @@
1
- import{replacer as e,reviver as r}from"./mls-json.js";let o=new TextEncoder,n=new TextDecoder;export function encodeRejoinRequest(r){return o.encode(JSON.stringify(r,e))}export function decodeRejoinRequest(e){let o=JSON.parse(n.decode(e),r);if("string"!=typeof o?.requestID)throw Error("invalid rejoin-request: missing requestID");return{requestID:o.requestID}}export function encodeRejoinGroupInfo(r){return o.encode(JSON.stringify(r,e))}export function decodeRejoinGroupInfo(e){let o=JSON.parse(n.decode(e),r);if("string"!=typeof o?.requestID)throw Error("invalid rejoin-groupinfo: missing requestID");if(!(o.groupInfo instanceof Uint8Array))throw Error("invalid rejoin-groupinfo: missing groupInfo bytes");return{requestID:o.requestID,groupInfo:o.groupInfo}}
@@ -1,34 +0,0 @@
1
- /**
2
- * Outer wire-frame discriminator for group hub payloads.
3
- *
4
- * The hub relay treats a group send payload as opaque bytes. Kubun prepends a
5
- * single `kind` byte so the receiver can route a payload to the right decoder
6
- * without trial-decryption:
7
- *
8
- * - `app` — an MLS-encrypted application broadcast (`GroupBroadcastMessage`),
9
- * decoded via `handle.decrypt` + `deserializeBroadcast`.
10
- * - `mls` — a framed MLS handshake message (a Commit), fed straight to
11
- * `handle.processMessage`. The handshake is self-protected (encrypted
12
- * `PrivateMessage`), so it is NOT wrapped in the application-encryption path.
13
- *
14
- * The discriminator lives entirely in kubun's payload bytes; the hub protocol
15
- * is unchanged.
16
- */
17
- export declare const WireKind: {
18
- readonly app: 0;
19
- readonly mls: 1;
20
- readonly rejoinRequest: 2;
21
- readonly rejoinGroupInfo: 3;
22
- };
23
- export type WireKind = (typeof WireKind)[keyof typeof WireKind];
24
- /** Prepend the `kind` byte to a payload. */
25
- export declare function frameWire(kind: WireKind, payload: Uint8Array): Uint8Array;
26
- /**
27
- * Split a framed payload into its `kind` and the remaining payload bytes.
28
- * Throws on an empty frame or an unrecognized `kind` byte. The returned
29
- * payload is a view (`subarray`) over the input, not a copy.
30
- */
31
- export declare function unframeWire(framed: Uint8Array): {
32
- kind: WireKind;
33
- payload: Uint8Array;
34
- };
@@ -1 +0,0 @@
1
- export const WireKind={app:0,mls:1,rejoinRequest:2,rejoinGroupInfo:3};let r=new Set(Object.values(WireKind));export function frameWire(r,e){let n=new Uint8Array(e.length+1);return n[0]=r,n.set(e,1),n}export function unframeWire(e){if(0===e.length)throw Error("empty wire frame");let n=e[0];if(!r.has(n))throw Error(`unknown wire frame kind: ${n}`);return{kind:n,payload:e.subarray(1)}}