@steve02081504/fount-p2p 0.0.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/README.md +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- package/wire/wait.mjs +127 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
import { pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
|
|
4
|
+
import { randomMsgIdHex } from '../link/frame.mjs'
|
|
5
|
+
import { createLruMap } from '../utils/lru.mjs'
|
|
6
|
+
|
|
7
|
+
const ROUTE_DOMAIN = 'fount-route-v1'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 构造 overlay 路由签名用的字节序列。
|
|
11
|
+
* @param {string} reqId 路由请求 id
|
|
12
|
+
* @param {string[]} path 已遍历节点路径
|
|
13
|
+
* @returns {Uint8Array} 待签名字节
|
|
14
|
+
*/
|
|
15
|
+
function routeSignBytes(reqId, path) {
|
|
16
|
+
return Buffer.from(`${ROUTE_DOMAIN}\0${reqId}\0${path.join(',')}`, 'utf8')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建 overlay 多跳路由与 relay 路由器。
|
|
21
|
+
* @param {object} registry link registry(含 localIdentity、sendToNodeLink、listLinks、subscribeScope)
|
|
22
|
+
* @param {number} [ttl=3] 默认路由 TTL(最大跳数)
|
|
23
|
+
* @returns {object} 路由器接口(discoverRoute、relay、onRelay、close)
|
|
24
|
+
*/
|
|
25
|
+
export function createOverlayRouter(registry, ttl = 3) {
|
|
26
|
+
const selfNodeHash = registry.localIdentity.nodeHash
|
|
27
|
+
const selfPubKey = registry.localIdentity.nodePubKey
|
|
28
|
+
const secretKey = registry.localIdentity.secretKey
|
|
29
|
+
const seenReqs = createLruMap(4096)
|
|
30
|
+
/** @type {Map<string, { resolve: (path: string[]) => void, reject: (err: Error) => void, timer: number }>} */
|
|
31
|
+
const pendingRoutes = new Map()
|
|
32
|
+
/** @type {Set<(body: unknown, meta: { path: string[], from: string }) => void>} */
|
|
33
|
+
const relayListeners = new Set()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 经 overlay scope 向节点发送 payload。
|
|
37
|
+
* @param {string} nodeHash 目标节点 64 hex
|
|
38
|
+
* @param {object} payload overlay action 载荷
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
async function sendOverlay(nodeHash, payload) {
|
|
42
|
+
await registry.sendToNodeLink(nodeHash, { scope: 'overlay', action: payload.action, payload })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 处理入站 overlay envelope(route_req / route_resp / relay)。
|
|
47
|
+
* @param {string} senderNodeHash 发送方节点 64 hex
|
|
48
|
+
* @param {object} envelope overlay 信封
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async function handleOverlay(senderNodeHash, envelope) {
|
|
52
|
+
const payload = envelope?.payload
|
|
53
|
+
const action = String(envelope?.action || '')
|
|
54
|
+
if (!payload || typeof payload !== 'object') return
|
|
55
|
+
if (action === 'route_req') {
|
|
56
|
+
const reqId = String(payload.reqId || '')
|
|
57
|
+
const target = String(payload.target || '')
|
|
58
|
+
const hops = Array.isArray(payload.path) ? payload.path.map(String) : []
|
|
59
|
+
const remainingTtl = Number(payload.ttl)
|
|
60
|
+
if (!reqId || !target || !hops.length || remainingTtl <= 0) return
|
|
61
|
+
if (seenReqs.has(reqId) || hops.includes(selfNodeHash) || hops.length > 6) return
|
|
62
|
+
seenReqs.touch(reqId, true)
|
|
63
|
+
const nextPath = [...hops, selfNodeHash]
|
|
64
|
+
if (target === selfNodeHash) {
|
|
65
|
+
const sig = await sign(routeSignBytes(reqId, nextPath), secretKey)
|
|
66
|
+
const prevHop = hops[hops.length - 1]
|
|
67
|
+
if (prevHop)
|
|
68
|
+
await sendOverlay(prevHop, {
|
|
69
|
+
action: 'route_resp',
|
|
70
|
+
reqId,
|
|
71
|
+
path: nextPath,
|
|
72
|
+
nodePubKey: selfPubKey,
|
|
73
|
+
sig: Buffer.from(sig).toString('hex'),
|
|
74
|
+
})
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
for (const { nodeHash } of registry.listLinks())
|
|
78
|
+
if (nodeHash !== senderNodeHash && !hops.includes(nodeHash))
|
|
79
|
+
await sendOverlay(nodeHash, {
|
|
80
|
+
action: 'route_req',
|
|
81
|
+
reqId,
|
|
82
|
+
target,
|
|
83
|
+
ttl: remainingTtl - 1,
|
|
84
|
+
path: nextPath,
|
|
85
|
+
})
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (action === 'route_resp') {
|
|
89
|
+
const reqId = String(payload.reqId || '')
|
|
90
|
+
const path = Array.isArray(payload.path) ? payload.path.map(String) : []
|
|
91
|
+
const nodePubKey = String(payload.nodePubKey || '')
|
|
92
|
+
const sigHex = String(payload.sig || '')
|
|
93
|
+
if (!reqId || path.length < 2 || !nodePubKey || !sigHex) return
|
|
94
|
+
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== path[path.length - 1]) return
|
|
95
|
+
const ok = await verify(Buffer.from(sigHex, 'hex'), routeSignBytes(reqId, path), Buffer.from(nodePubKey, 'hex'))
|
|
96
|
+
if (!ok) return
|
|
97
|
+
if (path[0] === selfNodeHash) {
|
|
98
|
+
const pending = pendingRoutes.get(reqId)
|
|
99
|
+
if (!pending) return
|
|
100
|
+
clearTimeout(pending.timer)
|
|
101
|
+
pendingRoutes.delete(reqId)
|
|
102
|
+
pending.resolve(path)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const index = path.indexOf(selfNodeHash)
|
|
106
|
+
if (index <= 0) return
|
|
107
|
+
await sendOverlay(path[index - 1], payload)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (action === 'relay') {
|
|
111
|
+
const path = Array.isArray(payload.path) ? payload.path.map(String) : []
|
|
112
|
+
const index = Number(payload.idx)
|
|
113
|
+
if (!path.length || path[index] !== selfNodeHash) return
|
|
114
|
+
if (index === path.length - 1) {
|
|
115
|
+
for (const listener of relayListeners)
|
|
116
|
+
listener(payload.body, { path, from: senderNodeHash })
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
await sendOverlay(path[index + 1], {
|
|
120
|
+
action: 'relay',
|
|
121
|
+
path,
|
|
122
|
+
idx: index + 1,
|
|
123
|
+
body: payload.body,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const unsubscribe = registry.subscribeScope('overlay', handleOverlay)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
/**
|
|
132
|
+
* 发现到目标节点的签名路由路径。
|
|
133
|
+
* @param {string} targetNodeHash 目标节点 64 hex
|
|
134
|
+
* @param {object} [opts] 选项
|
|
135
|
+
* @param {number} [opts.ttl] 路由 TTL
|
|
136
|
+
* @param {number} [opts.timeoutMs] 超时毫秒
|
|
137
|
+
* @returns {Promise<string[]>} 从本节点到目标的 nodeHash 路径
|
|
138
|
+
*/
|
|
139
|
+
async discoverRoute(targetNodeHash, opts = {}) {
|
|
140
|
+
const reqId = randomMsgIdHex()
|
|
141
|
+
const maxTtl = Number(opts.ttl) || ttl
|
|
142
|
+
const timeoutMs = Number(opts.timeoutMs) || 10_000
|
|
143
|
+
const promise = new Promise((resolve, reject) => {
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
pendingRoutes.delete(reqId)
|
|
146
|
+
reject(new Error(`overlay: route discovery timeout for ${targetNodeHash}`))
|
|
147
|
+
}, timeoutMs)
|
|
148
|
+
pendingRoutes.set(reqId, { resolve, reject, timer })
|
|
149
|
+
})
|
|
150
|
+
for (const { nodeHash } of registry.listLinks())
|
|
151
|
+
await sendOverlay(nodeHash, {
|
|
152
|
+
action: 'route_req',
|
|
153
|
+
reqId,
|
|
154
|
+
target: targetNodeHash,
|
|
155
|
+
ttl: maxTtl,
|
|
156
|
+
path: [selfNodeHash],
|
|
157
|
+
})
|
|
158
|
+
return await promise
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* 沿已发现路径 relay 载荷到路径末端。
|
|
162
|
+
* @param {string[]} path 路由路径(首节点须为本节点)
|
|
163
|
+
* @param {unknown} body relay 载荷
|
|
164
|
+
* @returns {Promise<void>}
|
|
165
|
+
*/
|
|
166
|
+
async relay(path, body) {
|
|
167
|
+
if (!Array.isArray(path) || path[0] !== selfNodeHash || path.length < 2)
|
|
168
|
+
throw new Error('overlay: invalid relay path')
|
|
169
|
+
await sendOverlay(path[1], { action: 'relay', path, idx: 1, body })
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* 订阅 relay 到达本节点(路径末端)的载荷。
|
|
173
|
+
* @param {(body: unknown, meta: { path: string[], from: string }) => void} listener 回调
|
|
174
|
+
* @returns {() => void} 取消订阅函数
|
|
175
|
+
*/
|
|
176
|
+
onRelay(listener) {
|
|
177
|
+
relayListeners.add(listener)
|
|
178
|
+
return () => relayListeners.delete(listener)
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* 关闭路由器并清理 pending 与监听器。
|
|
182
|
+
* @returns {void}
|
|
183
|
+
*/
|
|
184
|
+
close() {
|
|
185
|
+
unsubscribe()
|
|
186
|
+
for (const pending of pendingRoutes.values()) clearTimeout(pending.timer)
|
|
187
|
+
pendingRoutes.clear()
|
|
188
|
+
relayListeners.clear()
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@steve02081504/fount-p2p",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
|
+
"categories": [
|
|
6
|
+
"network",
|
|
7
|
+
"p2p",
|
|
8
|
+
"fount",
|
|
9
|
+
"AI"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/steve02081504/fount-p2p.git",
|
|
15
|
+
"directory": "."
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test --test-concurrency=1 --test-force-exit test/pure/*.test.mjs test/integration/*.test.mjs",
|
|
22
|
+
"test:live": "node --test --test-concurrency=1 --test-force-exit test/live/*.test.mjs",
|
|
23
|
+
"test:sim": "node --test --test-concurrency=1 --test-force-exit sim/test/*.test.mjs"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"**/*.mjs",
|
|
27
|
+
"**/*.json",
|
|
28
|
+
"!test/**",
|
|
29
|
+
"!sim/**",
|
|
30
|
+
"!scripts/**"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./index.mjs",
|
|
34
|
+
"./core/*": "./core/*.mjs",
|
|
35
|
+
"./crypto": "./crypto/crypto.mjs",
|
|
36
|
+
"./crypto/*": "./crypto/*.mjs",
|
|
37
|
+
"./wire/*": "./wire/*.mjs",
|
|
38
|
+
"./schemas/*": "./schemas/*.mjs",
|
|
39
|
+
"./node/*": "./node/*.mjs",
|
|
40
|
+
"./discovery": "./discovery/index.mjs",
|
|
41
|
+
"./discovery/*": "./discovery/*.mjs",
|
|
42
|
+
"./link/*": "./link/*.mjs",
|
|
43
|
+
"./transport/*": "./transport/*.mjs",
|
|
44
|
+
"./rooms/*": "./rooms/*.mjs",
|
|
45
|
+
"./overlay": "./overlay/index.mjs",
|
|
46
|
+
"./overlay/*": "./overlay/*.mjs",
|
|
47
|
+
"./entity/*": "./entity/*.mjs",
|
|
48
|
+
"./entity/files/*": "./entity/files/*.mjs",
|
|
49
|
+
"./trust_graph/*": "./trust_graph/*.mjs",
|
|
50
|
+
"./mailbox/*": "./mailbox/*.mjs",
|
|
51
|
+
"./dag": "./dag/index.mjs",
|
|
52
|
+
"./dag/*": "./dag/*.mjs",
|
|
53
|
+
"./timeline/*": "./timeline/*.mjs",
|
|
54
|
+
"./files/*": "./files/*.mjs",
|
|
55
|
+
"./federation/*": "./federation/*.mjs",
|
|
56
|
+
"./governance/*": "./governance/*.mjs",
|
|
57
|
+
"./registries/*": "./registries/*.mjs",
|
|
58
|
+
"./permissions": "./permissions/index.mjs",
|
|
59
|
+
"./reputation/*": "./reputation/*.mjs",
|
|
60
|
+
"./utils/*": "./utils/*.mjs"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@noble/curves": "^1.9.0",
|
|
64
|
+
"node-datachannel": "^0.11.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@steve02081504/exec": "file:../../exec"
|
|
68
|
+
},
|
|
69
|
+
"optionalDependencies": {
|
|
70
|
+
"@stoprocent/bleno": "^0.11.0",
|
|
71
|
+
"@stoprocent/noble": "^1.9.0"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用权限位图编解码。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {readonly string[]} order 权限名有序列表(位序固定)
|
|
7
|
+
* @returns {{ encode: (permissions: Record<string, boolean> | null | undefined) => bigint, decode: (bits: bigint) => Record<string, boolean> }} codec
|
|
8
|
+
*/
|
|
9
|
+
export function createPermissionCodec(order) {
|
|
10
|
+
const names = [...order]
|
|
11
|
+
/**
|
|
12
|
+
* @param {Record<string, boolean> | null | undefined} permissions 权限对象
|
|
13
|
+
* @returns {bigint} 按位编码
|
|
14
|
+
*/
|
|
15
|
+
function encode(permissions) {
|
|
16
|
+
let bits = 0n
|
|
17
|
+
if (!permissions) return bits
|
|
18
|
+
for (let index = 0; index < names.length; index++)
|
|
19
|
+
if (permissions[names[index]])
|
|
20
|
+
bits |= 1n << BigInt(index)
|
|
21
|
+
return bits
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @param {bigint} bits 权限位
|
|
25
|
+
* @returns {Record<string, boolean>} 各权限名到布尔值
|
|
26
|
+
*/
|
|
27
|
+
function decode(bits) {
|
|
28
|
+
/** @type {Record<string, boolean>} */
|
|
29
|
+
const permissions = {}
|
|
30
|
+
for (let index = 0; index < names.length; index++)
|
|
31
|
+
permissions[names[index]] = Boolean(bits & (1n << BigInt(index)))
|
|
32
|
+
return permissions
|
|
33
|
+
}
|
|
34
|
+
return { encode, decode }
|
|
35
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord 式「按特定度分层」权限求值器工厂。
|
|
3
|
+
*/
|
|
4
|
+
import { createPermissionCodec } from './bitmask.mjs'
|
|
5
|
+
import { applyDenyAllowOverride, mergeRoleOverrides } from './layered.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} LayeredEvaluatorSchema
|
|
9
|
+
* @property {readonly string[]} order 权限名有序列表
|
|
10
|
+
* @property {string} superuserName 旁路全部 scope 覆写的权限名(如 ADMIN)
|
|
11
|
+
* @property {string} [everyoneRoleId='@everyone'] scope 内最宽角色 id
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {LayeredEvaluatorSchema} schema 求值 schema
|
|
16
|
+
* @returns {{
|
|
17
|
+
* order: readonly string[],
|
|
18
|
+
* encode: (permissions: Record<string, boolean> | null | undefined) => bigint,
|
|
19
|
+
* decode: (bits: bigint) => Record<string, boolean>,
|
|
20
|
+
* calculate: (member: { roles?: string[] }, roles: Record<string, { permissions?: Record<string, boolean> }>, scopeId: string, scopeOverrides: Record<string, Record<string, { deny?: Record<string, boolean>, allow?: Record<string, boolean> }>>) => Record<string, boolean>,
|
|
21
|
+
* has: (member: object, permission: string, roles: object, scopeId: string, scopeOverrides: object) => boolean,
|
|
22
|
+
* }} evaluator
|
|
23
|
+
*/
|
|
24
|
+
export function createLayeredEvaluator(schema) {
|
|
25
|
+
const { encode, decode } = createPermissionCodec(schema.order)
|
|
26
|
+
const superuserBit = 1n << BigInt(schema.order.indexOf(schema.superuserName))
|
|
27
|
+
const everyoneRoleId = schema.everyoneRoleId ?? '@everyone'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {{ roles?: string[] }} member 成员
|
|
31
|
+
* @param {Record<string, { permissions?: Record<string, boolean> }>} roles 角色映射
|
|
32
|
+
* @param {string} scopeId scope id(如 channelId)
|
|
33
|
+
* @param {Record<string, Record<string, { deny?: Record<string, boolean>, allow?: Record<string, boolean> }>>} scopeOverrides scope 覆写表
|
|
34
|
+
* @returns {Record<string, boolean>} 最终权限
|
|
35
|
+
*/
|
|
36
|
+
function calculate(member, roles, scopeId, scopeOverrides) {
|
|
37
|
+
const roleIds = member.roles || []
|
|
38
|
+
|
|
39
|
+
let baseBits = 0n
|
|
40
|
+
for (const roleId of roleIds) {
|
|
41
|
+
const role = roles[roleId]
|
|
42
|
+
if (role) baseBits |= encode(role.permissions)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (baseBits & superuserBit) {
|
|
46
|
+
/** @type {Record<string, boolean>} */
|
|
47
|
+
const perms = {}
|
|
48
|
+
for (const p of schema.order) perms[p] = true
|
|
49
|
+
return perms
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let bits = baseBits
|
|
53
|
+
const scopeOverride = scopeOverrides?.[scopeId]
|
|
54
|
+
if (scopeOverride) {
|
|
55
|
+
const everyone = scopeOverride[everyoneRoleId]
|
|
56
|
+
if (everyone)
|
|
57
|
+
bits = applyDenyAllowOverride(bits, everyone, encode)
|
|
58
|
+
|
|
59
|
+
const roleOverrides = []
|
|
60
|
+
for (const roleId of roleIds) {
|
|
61
|
+
if (roleId === everyoneRoleId) continue
|
|
62
|
+
const override = scopeOverride[roleId]
|
|
63
|
+
if (override) roleOverrides.push(override)
|
|
64
|
+
}
|
|
65
|
+
bits = mergeRoleOverrides(bits, roleOverrides, encode)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return decode(bits)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {object} member 成员
|
|
73
|
+
* @param {string} permission 权限名
|
|
74
|
+
* @param {object} roles 角色映射
|
|
75
|
+
* @param {string} scopeId scope id
|
|
76
|
+
* @param {object} scopeOverrides scope 覆写表
|
|
77
|
+
* @returns {boolean} 是否具备权限
|
|
78
|
+
*/
|
|
79
|
+
function has(member, permission, roles, scopeId, scopeOverrides) {
|
|
80
|
+
return calculate(member, roles, scopeId, scopeOverrides)[permission] === true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
order: schema.order,
|
|
85
|
+
encode,
|
|
86
|
+
decode,
|
|
87
|
+
calculate,
|
|
88
|
+
has,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分层 allow/deny 覆写:先去 deny 再叠 allow。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {bigint} baseBits 基线权限位
|
|
7
|
+
* @param {{ deny?: Record<string, boolean> | null, allow?: Record<string, boolean> | null }} override 覆写
|
|
8
|
+
* @param {(permissions: Record<string, boolean> | null | undefined) => bigint} encode 编码函数
|
|
9
|
+
* @returns {bigint} 覆写后的权限位
|
|
10
|
+
*/
|
|
11
|
+
export function applyDenyAllowOverride(baseBits, override, encode) {
|
|
12
|
+
if (!override) return baseBits
|
|
13
|
+
let bits = baseBits
|
|
14
|
+
if (override.deny)
|
|
15
|
+
bits &= ~encode(override.deny)
|
|
16
|
+
if (override.allow)
|
|
17
|
+
bits |= encode(override.allow)
|
|
18
|
+
return bits
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 合并多个角色的 allow/deny 后整体应用(顺序无关)。
|
|
23
|
+
* @param {bigint} baseBits 基线权限位
|
|
24
|
+
* @param {Array<{ deny?: Record<string, boolean> | null, allow?: Record<string, boolean> | null }>} overrides 覆写列表
|
|
25
|
+
* @param {(permissions: Record<string, boolean> | null | undefined) => bigint} encode 编码函数
|
|
26
|
+
* @returns {bigint} 覆写后的权限位
|
|
27
|
+
*/
|
|
28
|
+
export function mergeRoleOverrides(baseBits, overrides, encode) {
|
|
29
|
+
let roleAllow = 0n
|
|
30
|
+
let roleDeny = 0n
|
|
31
|
+
for (const override of overrides) {
|
|
32
|
+
if (!override) continue
|
|
33
|
+
if (override.allow)
|
|
34
|
+
roleAllow |= encode(override.allow)
|
|
35
|
+
if (override.deny)
|
|
36
|
+
roleDeny |= encode(override.deny)
|
|
37
|
+
}
|
|
38
|
+
return (baseBits & ~roleDeny) | roleAllow
|
|
39
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 基于 room.makeAction(name) 的通用 action 注册表。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* P2P 房间 action 注册表,封装 makeAction 的 send/receive 对。
|
|
7
|
+
*/
|
|
8
|
+
export class ActionRegistry {
|
|
9
|
+
/** @type {Map<string, { send: Function, get: Function }>} */
|
|
10
|
+
#entries = new Map()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {{ makeAction: (name: string) => [Function, Function] }} room 提供 makeAction 的房间对象
|
|
14
|
+
*/
|
|
15
|
+
constructor(room) {
|
|
16
|
+
this.room = room
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 预注册一个或多个 action 名称。
|
|
21
|
+
* @param {string | string[]} names action 名称或名称列表
|
|
22
|
+
* @returns {ActionRegistry} 当前实例(链式调用)
|
|
23
|
+
*/
|
|
24
|
+
register(names) {
|
|
25
|
+
const list = Array.isArray(names) ? names : [names]
|
|
26
|
+
for (const name of list)
|
|
27
|
+
this.#ensureEntry(name)
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取或创建指定 action 的 send/get 条目。
|
|
33
|
+
* @param {string} name action 名称
|
|
34
|
+
* @returns {{ send: Function, get: Function }} send 与 get 函数对
|
|
35
|
+
*/
|
|
36
|
+
#ensureEntry(name) {
|
|
37
|
+
let entry = this.#entries.get(name)
|
|
38
|
+
if (!entry) {
|
|
39
|
+
const [send, get] = this.room.makeAction(name)
|
|
40
|
+
entry = { send, get }
|
|
41
|
+
this.#entries.set(name, entry)
|
|
42
|
+
}
|
|
43
|
+
return entry
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取指定 action 的发送函数。
|
|
48
|
+
* @param {string} name action 名称
|
|
49
|
+
* @returns {Function} 发送函数
|
|
50
|
+
*/
|
|
51
|
+
sender(name) {
|
|
52
|
+
return this.#ensureEntry(name).send
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 获取指定 action 的接收注册函数。
|
|
57
|
+
* @param {string} name action 名称
|
|
58
|
+
* @returns {Function} 接收注册函数
|
|
59
|
+
*/
|
|
60
|
+
receiver(name) {
|
|
61
|
+
return this.#ensureEntry(name).get
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 向指定 peer 发送 action 载荷。
|
|
66
|
+
* @param {string} name action 名称
|
|
67
|
+
* @param {unknown} payload 载荷
|
|
68
|
+
* @param {string | null} [peerId] 目标 peer;null 表示广播
|
|
69
|
+
* @returns {void}
|
|
70
|
+
*/
|
|
71
|
+
send(name, payload, peerId = null) {
|
|
72
|
+
void this.sender(name)(payload, peerId)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 注册 action 入站 handler。
|
|
77
|
+
* @param {string} name action 名称
|
|
78
|
+
* @param {(payload: unknown, peerId: string) => void} handler 入站回调
|
|
79
|
+
* @returns {ActionRegistry} 当前实例(链式调用)
|
|
80
|
+
*/
|
|
81
|
+
on(name, handler) {
|
|
82
|
+
this.receiver(name)(handler)
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 创建 ActionRegistry 实例。
|
|
89
|
+
* @param {{ makeAction: (name: string) => [Function, Function] }} room 提供 makeAction 的房间对象
|
|
90
|
+
* @returns {ActionRegistry} 新注册表
|
|
91
|
+
*/
|
|
92
|
+
export function createActionRegistry(room) {
|
|
93
|
+
return new ActionRegistry(room)
|
|
94
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} EventTypeFlags
|
|
3
|
+
* @property {boolean} [aclGated]
|
|
4
|
+
* @property {boolean} [gcExclude]
|
|
5
|
+
* @property {boolean} [governance]
|
|
6
|
+
* @property {boolean} [permissionAnchor]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @type {Map<string, Record<string, EventTypeFlags>>} */
|
|
10
|
+
const defsByOwner = new Map()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} ownerId 注册方
|
|
14
|
+
* @param {Record<string, EventTypeFlags>} defs 事件 type 元数据
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export function registerEventTypeDefs(ownerId, defs) {
|
|
18
|
+
defsByOwner.set(String(ownerId), defs)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} ownerId 注册方
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
export function unregisterEventTypeDefs(ownerId) {
|
|
26
|
+
defsByOwner.delete(String(ownerId))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @returns {void} */
|
|
30
|
+
export function clearEventTypeRegistry() {
|
|
31
|
+
defsByOwner.clear()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @returns {Record<string, EventTypeFlags>} 合并后的 defs
|
|
36
|
+
*/
|
|
37
|
+
export function mergedEventTypeDefs() {
|
|
38
|
+
/** @type {Record<string, EventTypeFlags>} */
|
|
39
|
+
const merged = {}
|
|
40
|
+
for (const defs of defsByOwner.values())
|
|
41
|
+
Object.assign(merged, defs)
|
|
42
|
+
return merged
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {'aclGated' | 'gcExclude' | 'governance' | 'permissionAnchor'} flag 标志位名
|
|
47
|
+
* @returns {Set<string>} 含该标志的事件 type 集合
|
|
48
|
+
*/
|
|
49
|
+
export function typesWithFlag(flag) {
|
|
50
|
+
return new Set(Object.entries(mergedEventTypeDefs()).filter(([, f]) => f[flag]).map(([k]) => k))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** §8 治理分叉选支:祖先闭包内计入信誉加权的类型。 */
|
|
54
|
+
/** @returns {Set<string>} 治理事件 type 集合 */
|
|
55
|
+
export function getGovernanceAuthzTypes() {
|
|
56
|
+
return typesWithFlag('governance')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 联邦入站/中继前须物化 ACL 门控的类型。 */
|
|
60
|
+
/** @returns {Set<string>} ACL 门控事件 type 集合 */
|
|
61
|
+
export function getFederationAclGatedEventTypes() {
|
|
62
|
+
return typesWithFlag('aclGated')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** §6.2 频道 GC 沉寂计时排除的类型。 */
|
|
66
|
+
/** @returns {Set<string>} GC 排除事件 type 集合 */
|
|
67
|
+
export function getChannelGcExcludedEventTypes() {
|
|
68
|
+
return typesWithFlag('gcExclude')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 裁剪时不得早于最早一条权限锚点事件(§7.1)。 */
|
|
72
|
+
/** @returns {Set<string>} 权限锚点事件 type 集合 */
|
|
73
|
+
export function getPermissionAnchorTypes() {
|
|
74
|
+
return typesWithFlag('permissionAnchor')
|
|
75
|
+
}
|