@kubun/plugin-p2p 0.11.0 → 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 (156) hide show
  1. package/lib/context/group.js +1073 -746
  2. package/lib/context/hub.js +2 -1
  3. package/lib/context/join.js +50 -15
  4. package/lib/context/peer.d.ts +92 -0
  5. package/lib/context/peer.js +1216 -0
  6. package/lib/context/require-admin.d.ts +57 -0
  7. package/lib/context/require-admin.js +57 -0
  8. package/lib/context/sync.d.ts +12 -0
  9. package/lib/context/sync.js +234 -1
  10. package/lib/context/types.d.ts +56 -20
  11. package/lib/context/types.js +23 -3
  12. package/lib/groups/access-default-token.d.ts +44 -0
  13. package/lib/groups/access-default-token.js +103 -0
  14. package/lib/groups/admin-roster.d.ts +42 -117
  15. package/lib/groups/admin-roster.js +53 -168
  16. package/lib/groups/anchor-store.d.ts +15 -0
  17. package/lib/groups/anchor-store.js +37 -0
  18. package/lib/groups/app-cursor-store.d.ts +17 -0
  19. package/lib/groups/app-cursor-store.js +34 -0
  20. package/lib/groups/broadcast.d.ts +199 -81
  21. package/lib/groups/broadcast.js +443 -368
  22. package/lib/groups/catalog-token.d.ts +50 -0
  23. package/lib/groups/catalog-token.js +96 -0
  24. package/lib/groups/circle-projection.d.ts +32 -64
  25. package/lib/groups/circle-projection.js +95 -181
  26. package/lib/groups/circle-reducers.d.ts +71 -110
  27. package/lib/groups/circle-reducers.js +57 -127
  28. package/lib/groups/commit-adoption.d.ts +77 -0
  29. package/lib/groups/commit-adoption.js +108 -0
  30. package/lib/groups/commit-journal.d.ts +14 -0
  31. package/lib/groups/commit-journal.js +65 -0
  32. package/lib/groups/control-request.d.ts +66 -0
  33. package/lib/groups/control-request.js +101 -0
  34. package/lib/groups/events.d.ts +14 -1
  35. package/lib/groups/group-anchor.d.ts +18 -57
  36. package/lib/groups/group-anchor.js +15 -100
  37. package/lib/groups/group-crypto.d.ts +36 -13
  38. package/lib/groups/group-crypto.js +124 -50
  39. package/lib/groups/group-handle-registry.d.ts +106 -36
  40. package/lib/groups/group-handle-registry.js +351 -61
  41. package/lib/groups/group-handlers.d.ts +5 -4
  42. package/lib/groups/group-handlers.js +155 -29
  43. package/lib/groups/group-health-monitor.d.ts +1 -1
  44. package/lib/groups/group-health-monitor.js +1 -1
  45. package/lib/groups/group-mls.d.ts +35 -4
  46. package/lib/groups/group-mls.js +594 -62
  47. package/lib/groups/group-peer-manager.d.ts +212 -19
  48. package/lib/groups/group-peer-manager.js +1061 -69
  49. package/lib/groups/group-protocols.d.ts +485 -28
  50. package/lib/groups/group-protocols.js +289 -15
  51. package/lib/groups/invite-payload.d.ts +48 -0
  52. package/lib/groups/join-request-identity.d.ts +21 -0
  53. package/lib/groups/join-request-identity.js +43 -0
  54. package/lib/groups/join-utils.d.ts +60 -0
  55. package/lib/groups/join-utils.js +262 -0
  56. package/lib/groups/ledger-affected-events.d.ts +11 -6
  57. package/lib/groups/ledger-affected-events.js +15 -26
  58. package/lib/groups/ledger-commit-fold.d.ts +107 -0
  59. package/lib/groups/ledger-commit-fold.js +167 -0
  60. package/lib/groups/ledger-ingest.d.ts +124 -0
  61. package/lib/groups/ledger-ingest.js +212 -0
  62. package/lib/groups/ledger.d.ts +44 -0
  63. package/lib/groups/ledger.js +30 -0
  64. package/lib/groups/manager.d.ts +202 -19
  65. package/lib/groups/manager.js +623 -106
  66. package/lib/groups/mls-codec.d.ts +27 -6
  67. package/lib/groups/mls-codec.js +30 -15
  68. package/lib/groups/mls-encryptor.js +9 -6
  69. package/lib/groups/mls-group-handle.d.ts +37 -2
  70. package/lib/groups/mls-group-handle.js +12 -4
  71. package/lib/groups/mls-receive-errors.d.ts +0 -2
  72. package/lib/groups/mls-receive-errors.js +3 -6
  73. package/lib/groups/mls-state.d.ts +5 -3
  74. package/lib/groups/mls-state.js +27 -4
  75. package/lib/groups/peer-presence.d.ts +90 -0
  76. package/lib/groups/peer-presence.js +295 -0
  77. package/lib/groups/peer-selection.d.ts +37 -0
  78. package/lib/groups/peer-selection.js +72 -0
  79. package/lib/groups/roster-projection.d.ts +53 -0
  80. package/lib/groups/roster-projection.js +65 -0
  81. package/lib/groups/store-received-grant.d.ts +4 -3
  82. package/lib/groups/store-received-grant.js +80 -35
  83. package/lib/groups/store-received-revocation.d.ts +16 -13
  84. package/lib/groups/store-received-revocation.js +23 -22
  85. package/lib/hub/http-client.d.ts +10 -27
  86. package/lib/hub/http-client.js +2 -19
  87. package/lib/hub/hub-like.d.ts +57 -6
  88. package/lib/hub/hub-like.js +539 -52
  89. package/lib/hub/loopback-log-hub.d.ts +20 -0
  90. package/lib/hub/loopback-log-hub.js +178 -0
  91. package/lib/hub/manager.d.ts +25 -40
  92. package/lib/hub/manager.js +51 -94
  93. package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
  94. package/lib/hub/peer-scoped-hub-view.js +92 -0
  95. package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
  96. package/lib/hub/sender-scoped-hub-view.js +59 -0
  97. package/lib/hub/server-did.d.ts +67 -0
  98. package/lib/hub/server-did.js +131 -0
  99. package/lib/hub/wiring.d.ts +71 -14
  100. package/lib/hub/wiring.js +80 -23
  101. package/lib/index.d.ts +48 -13
  102. package/lib/index.js +257 -86
  103. package/lib/peer/connection-registry.d.ts +45 -0
  104. package/lib/peer/connection-registry.js +52 -0
  105. package/lib/peer/handlers.d.ts +69 -0
  106. package/lib/peer/handlers.js +110 -0
  107. package/lib/protocol.d.ts +213 -0
  108. package/lib/protocol.js +268 -0
  109. package/lib/schema.d.ts +2 -1
  110. package/lib/schema.js +805 -131
  111. package/lib/sync/access-default-sender.d.ts +23 -0
  112. package/lib/sync/access-default-sender.js +80 -0
  113. package/lib/sync/authorize.d.ts +57 -3
  114. package/lib/sync/authorize.js +45 -4
  115. package/lib/sync/broadcast-queue.d.ts +3 -4
  116. package/lib/sync/broadcast-queue.js +2 -3
  117. package/lib/sync/catalog-scope.d.ts +8 -7
  118. package/lib/sync/catalog-scope.js +55 -39
  119. package/lib/sync/errors.d.ts +9 -0
  120. package/lib/sync/errors.js +10 -0
  121. package/lib/sync/forwarder.d.ts +13 -19
  122. package/lib/sync/forwarder.js +13 -19
  123. package/lib/sync/handlers.js +54 -14
  124. package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
  125. package/lib/sync/hub-tunnel-sync-listener.js +126 -37
  126. package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
  127. package/lib/sync/hub-tunnel-sync-provider.js +68 -19
  128. package/lib/sync/merkle-channel.d.ts +9 -1
  129. package/lib/sync/merkle-channel.js +0 -0
  130. package/lib/sync/peer-registry.d.ts +14 -10
  131. package/lib/sync/peer-registry.js +0 -25
  132. package/lib/sync/sync-client.d.ts +11 -1
  133. package/lib/sync/sync-client.js +72 -10
  134. package/lib/sync/sync-manager.d.ts +22 -4
  135. package/lib/sync/sync-manager.js +39 -10
  136. package/lib/sync/tunnel-listeners.d.ts +52 -0
  137. package/lib/sync/tunnel-listeners.js +175 -0
  138. package/lib/sync/tunnel-observability.d.ts +25 -0
  139. package/lib/sync/tunnel-observability.js +63 -0
  140. package/lib/sync/tunnel-topics.d.ts +20 -0
  141. package/lib/sync/tunnel-topics.js +28 -0
  142. package/lib/types.d.ts +699 -28
  143. package/lib/types.js +0 -1
  144. package/lib/util/handler-error.d.ts +16 -0
  145. package/lib/util/handler-error.js +64 -0
  146. package/lib/util/mutex.d.ts +14 -1
  147. package/lib/util/mutex.js +37 -3
  148. package/package.json +56 -52
  149. package/lib/groups/ledger-entry.d.ts +0 -37
  150. package/lib/groups/ledger-entry.js +0 -56
  151. package/lib/groups/ledger-fold.d.ts +0 -56
  152. package/lib/groups/ledger-fold.js +0 -53
  153. package/lib/hub/did-observing-transport.d.ts +0 -75
  154. package/lib/hub/did-observing-transport.js +0 -126
  155. package/lib/hub/errors.d.ts +0 -24
  156. package/lib/hub/errors.js +0 -33
package/lib/types.js CHANGED
@@ -1,2 +1 @@
1
- // ---- Plugin API ----
2
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,10 +1,44 @@
1
+ /** A waiter that gave up before its turn. Read by nothing: its `run` already rejected. */ const EXPIRED = Symbol('mutex.expired');
1
2
  export function createMutex() {
2
3
  let tail = Promise.resolve();
3
4
  return {
4
- run (fn) {
5
- const next = tail.then(()=>fn());
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
+ });
6
25
  tail = next.catch(()=>{});
7
- return next;
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
+ });
8
42
  }
9
43
  };
10
44
  }
package/package.json CHANGED
@@ -1,78 +1,82 @@
1
1
  {
2
2
  "name": "@kubun/plugin-p2p",
3
- "version": "0.11.0",
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
- "@sozai/async": "^0.1.0",
18
- "@kumiai/broadcast": "^0.1.0",
19
- "@kokuin/capability": "^0.1.0",
20
- "@enkaku/client": "^0.18.1",
21
- "@sozai/codec": "^0.1.0",
22
- "@sozai/event": "^0.1.0",
23
- "@sozai/generator": "^0.1.0",
24
- "@kumiai/mls": "^0.1.0",
25
- "@kumiai/rpc": "^0.1.0",
26
- "@enkaku/http-fetch": "^0.18.1",
27
- "@enkaku/http-serve": "^0.18.1",
28
- "@kumiai/hub-protocol": "^0.1.0",
29
- "@kumiai/hub-tunnel": "^0.1.0",
30
- "@enkaku/protocol": "^0.18.1",
31
- "@sozai/runtime": "^0.1.0",
32
- "@sozai/schema": "^0.1.0",
33
- "@enkaku/server": "^0.18.1",
34
- "@sozai/stream": "^0.1.0",
35
- "@kokuin/token": "^0.1.1",
36
- "@enkaku/transport": "^0.18.1",
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",
37
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",
38
41
  "graphql": "^16.14.2",
39
- "kysely": "^0.29.2",
42
+ "kysely": "^0.29.4",
40
43
  "ts-mls": "2.0.0-rc.13",
41
- "@kubun/db": "^0.11.0",
42
- "@kubun/db-adapter": "^0.11.0",
43
- "@kubun/graphql": "^0.11.0",
44
- "@kubun/hlc": "^0.11.0",
45
- "@kubun/engine": "^0.11.0",
46
- "@kubun/http-util": "^0.11.0",
47
- "@kubun/logger": "^0.11.0",
48
- "@kubun/client": "^0.11.0",
49
- "@kubun/id": "^0.11.0",
50
- "@kubun/mutation": "^0.11.0",
51
- "@kubun/store-graph": "^0.11.0",
52
- "@kubun/protocol": "^0.11.0",
53
- "@kubun/store-delegation": "^0.11.0",
54
- "@kubun/store-p2p": "^0.11.0"
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"
55
59
  },
56
60
  "devDependencies": {
57
- "@kumiai/hub-server": "^0.1.0",
58
- "@testcontainers/postgresql": "^12.0.4",
61
+ "@kumiai/hub-conformance": "^0.5.0",
62
+ "@kumiai/rpc-conformance": "^0.5.0",
63
+ "@testcontainers/postgresql": "^12.1.0",
59
64
  "get-port": "^7.2.0",
60
- "@kubun/db-better-sqlite": "^0.11.0",
61
- "@kubun/db-node-sqlite": "^0.11.0",
62
- "@kubun/plugin-connector": "^0.11.0",
63
- "@kubun/db-postgres": "^0.11.0",
64
- "@kubun/plugin-rpc": "^0.11.0",
65
- "@kubun/plugin-http": "^0.11.0",
66
- "@kubun/hub": "^0.11.0",
67
- "@kubun/test-utils": "^0.11.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"
68
72
  },
69
73
  "scripts": {
74
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
70
75
  "build:clean": "del lib",
71
76
  "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
72
77
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
73
- "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
78
+ "test": "pnpm run test:types && pnpm run test:unit",
74
79
  "test:types": "tsc --noEmit -p tsconfig.test.json",
75
- "test:unit": "vitest run",
76
- "test": "pnpm run test:types && pnpm run test:unit"
80
+ "test:unit": "vitest run"
77
81
  }
78
82
  }
@@ -1,37 +0,0 @@
1
- import { type OwnIdentity } from '@kokuin/token';
2
- /**
3
- * A control-ledger claim. The payload of a signed ledger token: `type` selects
4
- * the reducer, `subject` is the entity the claim is about, `value` carries the
5
- * claim data, and `hlc` orders it. The authenticated author is never part of
6
- * this shape — it comes from the verified token's `iss`.
7
- */
8
- export type LedgerEntry<TValue = unknown> = {
9
- type: string;
10
- subject: string;
11
- value: TValue;
12
- hlc: string;
13
- };
14
- /** A ledger entry whose token signature has been cryptographically verified. */
15
- export type VerifiedLedgerEntry<TValue = unknown> = {
16
- /** Authenticated author DID (the verified token issuer), normalized. */
17
- issuer: string;
18
- entry: LedgerEntry<TValue>;
19
- };
20
- /**
21
- * Sign a ledger entry. The signer fills `iss` with its DID, so the signature
22
- * covers every claim field and binds the author. Returns the stringified token.
23
- */
24
- export declare function signLedgerEntry(identity: OwnIdentity, entry: LedgerEntry): Promise<string>;
25
- /**
26
- * Verify a signed ledger token and extract its claim. Returns `null` (never
27
- * throws) when the token is unparseable, unsigned (`alg: 'none'` — `verifyToken`
28
- * returns those without checking a signature, so an attacker could forge an
29
- * arbitrary `iss`), or structurally malformed. The claim fields come from the
30
- * verified payload only; the issuer is the normalized verified `iss`.
31
- */
32
- export declare function verifyLedgerEntry<TValue = unknown>(token: string): Promise<VerifiedLedgerEntry<TValue> | null>;
33
- /**
34
- * Content-addressed digest of a signed ledger token, used as the append-only
35
- * store's dedup key. Multibase-encoded SHA-256 multihash over the token bytes.
36
- */
37
- export declare function ledgerEntryDigest(signedToken: string): string;
@@ -1,56 +0,0 @@
1
- import { encodeMultibase, isVerifiedToken, multihashSHA256, normalizeDID, stringifyToken, verifyToken } from '@kokuin/token';
2
- const textEncoder = new TextEncoder();
3
- /**
4
- * Sign a ledger entry. The signer fills `iss` with its DID, so the signature
5
- * covers every claim field and binds the author. Returns the stringified token.
6
- */ export async function signLedgerEntry(identity, entry) {
7
- const signed = await identity.signToken({
8
- type: entry.type,
9
- subject: entry.subject,
10
- value: entry.value,
11
- hlc: entry.hlc
12
- }, // Embed the long-form DID so each entry is self-verifying offline: the
13
- // ledger is the source of truth and receivers fold it long after first
14
- // contact, when the author's DID document may no longer be cached. No-op
15
- // for did:key (long form === id).
16
- {
17
- embedLongForm: true
18
- });
19
- return stringifyToken(signed);
20
- }
21
- /**
22
- * Verify a signed ledger token and extract its claim. Returns `null` (never
23
- * throws) when the token is unparseable, unsigned (`alg: 'none'` — `verifyToken`
24
- * returns those without checking a signature, so an attacker could forge an
25
- * arbitrary `iss`), or structurally malformed. The claim fields come from the
26
- * verified payload only; the issuer is the normalized verified `iss`.
27
- */ export async function verifyLedgerEntry(token) {
28
- let verified;
29
- try {
30
- verified = await verifyToken(token);
31
- } catch {
32
- return null;
33
- }
34
- if (!isVerifiedToken(verified)) {
35
- return null;
36
- }
37
- const { iss, type, subject, value, hlc } = verified.payload;
38
- if (typeof type !== 'string' || typeof subject !== 'string' || typeof hlc !== 'string') {
39
- return null;
40
- }
41
- return {
42
- issuer: normalizeDID(iss),
43
- entry: {
44
- type,
45
- subject,
46
- value,
47
- hlc
48
- }
49
- };
50
- }
51
- /**
52
- * Content-addressed digest of a signed ledger token, used as the append-only
53
- * store's dedup key. Multibase-encoded SHA-256 multihash over the token bytes.
54
- */ export function ledgerEntryDigest(signedToken) {
55
- return encodeMultibase(multihashSHA256(textEncoder.encode(signedToken)));
56
- }
@@ -1,56 +0,0 @@
1
- import type { GroupAnchor } from './group-anchor.js';
2
- import type { VerifiedLedgerEntry } from './ledger-entry.js';
3
- /**
4
- * A per-type projection over the control ledger. The fold stays free of any
5
- * admin/circle semantics: a reducer owns its own initial state (derived from
6
- * the genesis anchor), its authority rule, and its fold step, so a new ledger
7
- * type plugs in as a parameter with zero edits to {@link foldLedger}.
8
- */
9
- export type LedgerReducer<TValue, TState> = {
10
- /** Ledger entry `type` this reducer projects; entries of any other type are dropped. */
11
- type: string;
12
- /**
13
- * Initial fold state, derived from the genesis anchor. The anchor is the
14
- * authenticated epoch-0 root (e.g. the creator DID is the first admin), so
15
- * seeding from it is what makes the very first claims evaluable.
16
- */
17
- seed(anchor: GroupAnchor): TState;
18
- /** Is the verified issuer allowed to make this claim, given the state so far? */
19
- verifyAuthority(verified: VerifiedLedgerEntry<TValue>, stateSoFar: TState): boolean;
20
- /** Fold step: return the next state after applying an authorized claim. */
21
- apply(verified: VerifiedLedgerEntry<TValue>, stateSoFar: TState): TState;
22
- };
23
- /**
24
- * A verified entry paired with its content-addressed id. The id is the
25
- * total-order tie-breaker when two entries share an `hlc`, and identifies the
26
- * entry in drop notices.
27
- */
28
- export type FoldInput<TValue = unknown> = {
29
- verified: VerifiedLedgerEntry<TValue>;
30
- entryID: string;
31
- };
32
- /** Why an entry was skipped during the fold, for an optional observer. */
33
- export type FoldDrop = {
34
- entryID: string;
35
- type: string;
36
- reason: string;
37
- };
38
- /**
39
- * Replay the ledger into a single reducer's projection.
40
- *
41
- * Signatures are verified at ingest, not here — this fold evaluates AUTHORITY:
42
- * whether each issuer was allowed to make its claim given the state accumulated
43
- * from strictly-earlier entries. Evaluating against state-so-far (never the
44
- * final state) is what makes rotation sound: a key authorized at HLC `t` can be
45
- * revoked at `t+1` without retroactively invalidating its earlier claims.
46
- *
47
- * Deterministic on any arrival order: it seeds from the genesis anchor, sorts a
48
- * copy of the entries by the total order `(hlc, entryID)`, then folds. Pure — no
49
- * clock, no randomness, no I/O, and the input array is never mutated. A claim of
50
- * an unrelated type or one whose authority fails is dropped (never thrown) so a
51
- * single bad entry can never abort the projection. Drops are silent unless an
52
- * `onDrop` observer is supplied — the fold runs on every authority check, where
53
- * authority-failed drops are expected, so the caller decides whether to surface
54
- * them rather than the fold emitting to the console.
55
- */
56
- export declare function foldLedger<TValue, TState>(entries: Array<FoldInput<TValue>>, anchor: GroupAnchor, reducer: LedgerReducer<TValue, TState>, onDrop?: (drop: FoldDrop) => void): TState;
@@ -1,53 +0,0 @@
1
- /** Total order over the ledger: HLC first, then content-addressed id to break ties. */ function compareFoldInputs(a, b) {
2
- if (a.verified.entry.hlc !== b.verified.entry.hlc) {
3
- return a.verified.entry.hlc < b.verified.entry.hlc ? -1 : 1;
4
- }
5
- if (a.entryID === b.entryID) {
6
- return 0;
7
- }
8
- return a.entryID < b.entryID ? -1 : 1;
9
- }
10
- /**
11
- * Replay the ledger into a single reducer's projection.
12
- *
13
- * Signatures are verified at ingest, not here — this fold evaluates AUTHORITY:
14
- * whether each issuer was allowed to make its claim given the state accumulated
15
- * from strictly-earlier entries. Evaluating against state-so-far (never the
16
- * final state) is what makes rotation sound: a key authorized at HLC `t` can be
17
- * revoked at `t+1` without retroactively invalidating its earlier claims.
18
- *
19
- * Deterministic on any arrival order: it seeds from the genesis anchor, sorts a
20
- * copy of the entries by the total order `(hlc, entryID)`, then folds. Pure — no
21
- * clock, no randomness, no I/O, and the input array is never mutated. A claim of
22
- * an unrelated type or one whose authority fails is dropped (never thrown) so a
23
- * single bad entry can never abort the projection. Drops are silent unless an
24
- * `onDrop` observer is supplied — the fold runs on every authority check, where
25
- * authority-failed drops are expected, so the caller decides whether to surface
26
- * them rather than the fold emitting to the console.
27
- */ export function foldLedger(entries, anchor, reducer, onDrop) {
28
- const ordered = [
29
- ...entries
30
- ].sort(compareFoldInputs);
31
- let state = reducer.seed(anchor);
32
- for (const { verified, entryID } of ordered){
33
- const { entry } = verified;
34
- if (entry.type !== reducer.type) {
35
- onDrop?.({
36
- entryID,
37
- type: entry.type,
38
- reason: `unknown type for reducer '${reducer.type}'`
39
- });
40
- continue;
41
- }
42
- if (!reducer.verifyAuthority(verified, state)) {
43
- onDrop?.({
44
- entryID,
45
- type: entry.type,
46
- reason: `issuer '${verified.issuer}' not authorized`
47
- });
48
- continue;
49
- }
50
- state = reducer.apply(verified, state);
51
- }
52
- return state;
53
- }
@@ -1,75 +0,0 @@
1
- import type { AnyClientMessageOf, AnyServerMessageOf, ClientTransportOf, ProtocolDefinition } from '@enkaku/protocol';
2
- import type { TransportEvents } from '@enkaku/transport';
3
- import { Disposer } from '@sozai/async';
4
- import type { EventEmitter } from '@sozai/event';
5
- export type ServerDIDObserver = (serverDID: string) => void | Promise<void>;
6
- export type ServerDIDMismatchObserver = (params: {
7
- hubURL: string;
8
- expectedServerDID: string;
9
- observedServerDID: string;
10
- }) => void;
11
- /**
12
- * Fired once per freshly-spawned hub client when its first signed response
13
- * arrives. The hub URL is bound into the call site; consumers (hub manager
14
- * `captureServerDID`) use the URL to look up the local hub row and apply the
15
- * TOFU conflict matrix. Errors are caught and logged — capture never blocks
16
- * message forwarding, even on `HubServerDIDConflictError`.
17
- */
18
- export type OnServerDIDObserved = (params: {
19
- hubURL: string;
20
- serverDID: string;
21
- }) => void | Promise<void>;
22
- export type DIDObservingTransportParams<Protocol extends ProtocolDefinition> = {
23
- inner: ClientTransportOf<Protocol>;
24
- /**
25
- * Fired on the first message bearing `payload.iss` when no `expectedServerDID`
26
- * is set (TOFU capture). When `expectedServerDID` is set and matches, the
27
- * callback is NOT fired — the pin is already authoritative.
28
- */
29
- onServerDID: ServerDIDObserver;
30
- /**
31
- * Pinned hub server DID. When set, the wrapper enforces it: a non-matching
32
- * first observed `iss` triggers a permanent failed state — `read()` rejects
33
- * with `HubServerDIDMismatchError`, the offending message is NOT delivered
34
- * to the inner Client, and every subsequent `read()` re-raises the same
35
- * error. When unset, the wrapper falls back to TOFU capture.
36
- */
37
- expectedServerDID?: string;
38
- /** Hub URL — only used to populate `HubServerDIDMismatchError` for callers. */
39
- hubURL?: string;
40
- /**
41
- * Optional side-channel notification fired on mismatch. Lets the pool log /
42
- * evict without having to inspect the rejected `read()` cause. Inner
43
- * transport disposal is the pool's job, not the wrapper's.
44
- */
45
- onMismatch?: ServerDIDMismatchObserver;
46
- };
47
- /**
48
- * Wraps a `ClientTransportOf<Protocol>` to enforce hub-server-DID
49
- * TOFU + pinned-mismatch semantics on the receive side.
50
- *
51
- * - `expectedServerDID == null`: TOFU mode. First message bearing
52
- * `payload.iss` fires `onServerDID(iss)` exactly once; subsequent messages
53
- * never re-fire, even with a differing `iss`. Every message is forwarded
54
- * to the inner Client untouched.
55
- * - `expectedServerDID != null` and the first observed `iss` matches: the
56
- * pin is already authoritative; `onServerDID` is NOT fired and forwarding
57
- * continues normally.
58
- * - `expectedServerDID != null` and the first observed `iss` differs: the
59
- * wrapper enters a permanent failed state (`HubServerDIDMismatchError`).
60
- * The current `read()` call AND every subsequent `read()` reject with the
61
- * error; the offending message is NOT delivered. Inner-transport lifecycle
62
- * stays the pool's responsibility — the wrapper does not dispose its inner.
63
- *
64
- * Extends `Disposer` and matches the `ClientTransportOf<Protocol>` shape
65
- * structurally so it can be passed straight to `Client<Protocol>`.
66
- */
67
- export declare class DIDObservingTransport<Protocol extends ProtocolDefinition> extends Disposer {
68
- #private;
69
- constructor(params: DIDObservingTransportParams<Protocol>);
70
- get events(): EventEmitter<TransportEvents>;
71
- getWritable(): WritableStream<AnyClientMessageOf<Protocol>>;
72
- read(): Promise<ReadableStreamReadResult<AnyServerMessageOf<Protocol>>>;
73
- write(value: AnyClientMessageOf<Protocol>): Promise<void>;
74
- [Symbol.asyncIterator](): AsyncIterator<AnyServerMessageOf<Protocol>, AnyServerMessageOf<Protocol> | null>;
75
- }