@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.
- package/lib/context/group.js +1073 -746
- package/lib/context/hub.js +2 -1
- package/lib/context/join.js +50 -15
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +234 -1
- package/lib/context/types.d.ts +56 -20
- package/lib/context/types.js +23 -3
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +42 -117
- package/lib/groups/admin-roster.js +53 -168
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast.d.ts +199 -81
- package/lib/groups/broadcast.js +443 -368
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +32 -64
- package/lib/groups/circle-projection.js +95 -181
- package/lib/groups/circle-reducers.d.ts +71 -110
- package/lib/groups/circle-reducers.js +57 -127
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +14 -1
- package/lib/groups/group-anchor.d.ts +18 -57
- package/lib/groups/group-anchor.js +15 -100
- package/lib/groups/group-crypto.d.ts +36 -13
- package/lib/groups/group-crypto.js +124 -50
- package/lib/groups/group-handle-registry.d.ts +106 -36
- package/lib/groups/group-handle-registry.js +351 -61
- package/lib/groups/group-handlers.d.ts +5 -4
- package/lib/groups/group-handlers.js +155 -29
- package/lib/groups/group-health-monitor.d.ts +1 -1
- package/lib/groups/group-health-monitor.js +1 -1
- package/lib/groups/group-mls.d.ts +35 -4
- package/lib/groups/group-mls.js +594 -62
- package/lib/groups/group-peer-manager.d.ts +212 -19
- package/lib/groups/group-peer-manager.js +1061 -69
- package/lib/groups/group-protocols.d.ts +485 -28
- package/lib/groups/group-protocols.js +289 -15
- package/lib/groups/invite-payload.d.ts +48 -0
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +60 -0
- package/lib/groups/join-utils.js +262 -0
- package/lib/groups/ledger-affected-events.d.ts +11 -6
- package/lib/groups/ledger-affected-events.js +15 -26
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +202 -19
- package/lib/groups/manager.js +623 -106
- package/lib/groups/mls-codec.d.ts +27 -6
- package/lib/groups/mls-codec.js +30 -15
- package/lib/groups/mls-encryptor.js +9 -6
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +12 -4
- package/lib/groups/mls-receive-errors.d.ts +0 -2
- package/lib/groups/mls-receive-errors.js +3 -6
- package/lib/groups/mls-state.d.ts +5 -3
- package/lib/groups/mls-state.js +27 -4
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +4 -3
- package/lib/groups/store-received-grant.js +80 -35
- package/lib/groups/store-received-revocation.d.ts +16 -13
- package/lib/groups/store-received-revocation.js +23 -22
- package/lib/hub/http-client.d.ts +10 -27
- package/lib/hub/http-client.js +2 -19
- package/lib/hub/hub-like.d.ts +57 -6
- package/lib/hub/hub-like.js +539 -52
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +51 -94
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +71 -14
- package/lib/hub/wiring.js +80 -23
- package/lib/index.d.ts +48 -13
- package/lib/index.js +257 -86
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +213 -0
- package/lib/protocol.js +268 -0
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +805 -131
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +57 -3
- package/lib/sync/authorize.js +45 -4
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +2 -3
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +55 -39
- package/lib/sync/errors.d.ts +9 -0
- package/lib/sync/errors.js +10 -0
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +13 -19
- package/lib/sync/handlers.js +54 -14
- package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
- package/lib/sync/hub-tunnel-sync-listener.js +126 -37
- package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
- package/lib/sync/hub-tunnel-sync-provider.js +68 -19
- package/lib/sync/merkle-channel.d.ts +9 -1
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/peer-registry.d.ts +14 -10
- package/lib/sync/peer-registry.js +0 -25
- package/lib/sync/sync-client.d.ts +11 -1
- package/lib/sync/sync-client.js +72 -10
- package/lib/sync/sync-manager.d.ts +22 -4
- package/lib/sync/sync-manager.js +39 -10
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +699 -28
- package/lib/types.js +0 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +37 -3
- package/package.json +56 -52
- package/lib/groups/ledger-entry.d.ts +0 -37
- package/lib/groups/ledger-entry.js +0 -56
- package/lib/groups/ledger-fold.d.ts +0 -56
- package/lib/groups/ledger-fold.js +0 -53
- package/lib/hub/did-observing-transport.d.ts +0 -75
- package/lib/hub/did-observing-transport.js +0 -126
- package/lib/hub/errors.d.ts +0 -24
- package/lib/hub/errors.js +0 -33
package/lib/types.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/util/mutex.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
"@
|
|
18
|
-
"@
|
|
19
|
-
"@
|
|
20
|
-
"@enkaku/
|
|
21
|
-
"@
|
|
22
|
-
"@
|
|
23
|
-
"@
|
|
24
|
-
"@
|
|
25
|
-
"@kumiai/
|
|
26
|
-
"@
|
|
27
|
-
"@
|
|
28
|
-
"@kumiai/hub-
|
|
29
|
-
"@kumiai/
|
|
30
|
-
"@
|
|
31
|
-
"@
|
|
32
|
-
"@
|
|
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.
|
|
42
|
+
"kysely": "^0.29.4",
|
|
40
43
|
"ts-mls": "2.0.0-rc.13",
|
|
41
|
-
"@kubun/db": "^0.
|
|
42
|
-
"@kubun/db-adapter": "^0.
|
|
43
|
-
"@kubun/
|
|
44
|
-
"@kubun/
|
|
45
|
-
"@kubun/
|
|
46
|
-
"@kubun/http-util": "^0.
|
|
47
|
-
"@kubun/logger": "^0.
|
|
48
|
-
"@kubun/
|
|
49
|
-
"@kubun/
|
|
50
|
-
"@kubun/
|
|
51
|
-
"@kubun/
|
|
52
|
-
"@kubun/
|
|
53
|
-
"@kubun/
|
|
54
|
-
"@kubun/store-
|
|
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-
|
|
58
|
-
"@
|
|
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-
|
|
61
|
-
"@kubun/
|
|
62
|
-
"@kubun/plugin-connector": "^0.
|
|
63
|
-
"@kubun/
|
|
64
|
-
"@kubun/
|
|
65
|
-
"@kubun/
|
|
66
|
-
"@kubun/
|
|
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
|
-
"
|
|
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
|
-
}
|