@steve02081504/fount-p2p 0.0.2 → 0.0.4

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.
@@ -0,0 +1,153 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ import { canonicalStringify } from '../core/canonical_json.mjs'
4
+ import { hashFromPubKeyHex, parseEntityHash } from '../core/entity_id.mjs'
5
+ import { assertSafeEvfsLogicalPath } from '../core/evfs_logical_path.mjs'
6
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
7
+ import { sign, verify } from '../crypto/crypto.mjs'
8
+
9
+ import { putFileManifest, saveFileManifest } from './evfs.mjs'
10
+ import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest.mjs'
11
+
12
+ /** 实体公开 manifest 签名域 */
13
+ export const ENTITY_PUBLIC_MANIFEST_DOMAIN = 'fount-entity-public-manifest'
14
+
15
+ /**
16
+ * @param {object} fields 待签名字段
17
+ * @returns {Buffer} 签名消息字节
18
+ */
19
+ export function publicManifestSignBytes(fields) {
20
+ return Buffer.from(canonicalStringify([
21
+ ENTITY_PUBLIC_MANIFEST_DOMAIN,
22
+ String(fields.ownerEntityHash || '').trim().toLowerCase(),
23
+ assertSafeEvfsLogicalPath(fields.logicalPath),
24
+ Number(fields.publishedAt) || 0,
25
+ String(fields.contentHash || '').trim().toLowerCase(),
26
+ Number(fields.size) || 0,
27
+ String(fields.mimeType || ''),
28
+ String(fields.name || ''),
29
+ String(fields.ceMode || ''),
30
+ fields.parts || [],
31
+ ]), 'utf8')
32
+ }
33
+
34
+ /**
35
+ * @param {import('./manifest.mjs').FileManifest} manifest 清单
36
+ * @param {number} publishedAt 发布时间
37
+ * @param {Uint8Array | Buffer} entitySecretKey recovery 私钥种子
38
+ * @param {string} entityPubKeyHex recovery 公钥 hex
39
+ * @returns {Promise<import('./manifest.mjs').FileManifest>} 带 publicSig 的清单
40
+ */
41
+ export async function attachPublicManifestSig(manifest, publishedAt, entitySecretKey, entityPubKeyHex) {
42
+ const pubKeyHex = normalizeHex64(entityPubKeyHex)
43
+ const message = publicManifestSignBytes({
44
+ ownerEntityHash: manifest.ownerEntityHash,
45
+ logicalPath: manifest.logicalPath,
46
+ publishedAt,
47
+ contentHash: manifest.contentHash,
48
+ size: manifest.size,
49
+ mimeType: manifest.mimeType,
50
+ name: manifest.name,
51
+ ceMode: manifest.ceMode,
52
+ parts: manifest.parts,
53
+ })
54
+ const sigHex = Buffer.from(await sign(message, entitySecretKey)).toString('hex')
55
+ return {
56
+ ...manifest,
57
+ meta: {
58
+ ...manifest.meta || {},
59
+ publicSig: { publishedAt, pubKeyHex, sigHex },
60
+ },
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param {unknown} input 原始 manifest
66
+ * @returns {Promise<import('./manifest.mjs').FileManifest | null>} 验签通过的清单;非法为 null
67
+ */
68
+ export async function verifySignedPublicManifest(input) {
69
+ const manifest = normalizeFileManifest(input)
70
+ if (!manifest) return null
71
+ if (manifest.transferKeyDescriptor.type !== 'public') return null
72
+
73
+ const publicSig = input?.meta?.publicSig
74
+ if (!publicSig || typeof publicSig !== 'object') return null
75
+ const publishedAt = Number(publicSig.publishedAt) || 0
76
+ const pubKeyHex = normalizeHex64(publicSig.pubKeyHex)
77
+ const sigHex = String(publicSig.sigHex || '').trim().toLowerCase()
78
+ if (!isHex64(pubKeyHex) || !/^[\da-f]{128}$/u.test(sigHex) || publishedAt <= 0) return null
79
+
80
+ const parsed = parseEntityHash(manifest.ownerEntityHash)
81
+ if (!parsed) return null
82
+ if (hashFromPubKeyHex(pubKeyHex) !== parsed.subjectHash) return null
83
+
84
+ const message = publicManifestSignBytes({
85
+ ownerEntityHash: manifest.ownerEntityHash,
86
+ logicalPath: manifest.logicalPath,
87
+ publishedAt,
88
+ contentHash: manifest.contentHash,
89
+ size: manifest.size,
90
+ mimeType: manifest.mimeType,
91
+ name: manifest.name,
92
+ ceMode: manifest.ceMode,
93
+ parts: manifest.parts,
94
+ })
95
+ const ok = await verify(Buffer.from(sigHex, 'hex'), message, Buffer.from(pubKeyHex, 'hex'))
96
+ if (!ok) return null
97
+
98
+ // 签名只覆盖内容字段:入站 meta 一律丢弃,仅保留 publicSig,
99
+ // 防止中继注入 dagParts/groupId 等本地扩展改写读取路径。
100
+ return {
101
+ ...manifest,
102
+ meta: { publicSig: { publishedAt, pubKeyHex, sigHex } },
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {object | null | undefined} localManifest 本地已有清单
108
+ * @param {import('./manifest.mjs').FileManifest} incoming 入站已验签清单
109
+ * @returns {boolean} 是否应以 incoming 覆盖本地缓存
110
+ */
111
+ export function shouldPreferIncomingPublicManifest(localManifest, incoming) {
112
+ const incomingAt = Number(incoming?.meta?.publicSig?.publishedAt) || 0
113
+ if (incomingAt <= 0) return false
114
+ const localAt = Number(localManifest?.meta?.publicSig?.publishedAt) || 0
115
+ return incomingAt > localAt
116
+ }
117
+
118
+ /**
119
+ * @param {object} params 参数
120
+ * @param {string} params.ownerEntityHash owner
121
+ * @param {string} params.logicalPath 路径
122
+ * @param {Buffer | Uint8Array} params.plaintext 明文
123
+ * @param {string} [params.name] 文件名
124
+ * @param {string} [params.mimeType] MIME
125
+ * @param {Uint8Array | Buffer} params.entitySecretKey recovery 私钥种子
126
+ * @param {string} params.entityPubKeyHex recovery 公钥 hex
127
+ * @param {number} [params.publishedAt] 发布时间(默认 Date.now)
128
+ * @returns {Promise<import('./manifest.mjs').FileManifest>} 已签名并落盘的公开清单
129
+ */
130
+ export async function publishPublicFile(params) {
131
+ const {
132
+ ownerEntityHash,
133
+ logicalPath,
134
+ plaintext,
135
+ name,
136
+ mimeType,
137
+ entitySecretKey,
138
+ entityPubKeyHex,
139
+ publishedAt = Date.now(),
140
+ } = params
141
+ const base = await putFileManifest({
142
+ ownerEntityHash,
143
+ logicalPath,
144
+ plaintext,
145
+ name,
146
+ mimeType,
147
+ ceMode: 'convergent',
148
+ transferKeyDescriptor: publicTransferKeyDescriptor(),
149
+ })
150
+ const signed = await attachPublicManifestSig(base, publishedAt, entitySecretKey, entityPubKeyHex)
151
+ await saveFileManifest(signed)
152
+ return signed
153
+ }
@@ -43,6 +43,8 @@ const DEFAULT_ACTION_PRIORITIES = Object.freeze({
43
43
  fed_chunk_get: 5,
44
44
  fed_chunk_data: 5,
45
45
  fed_chunk_ack: 5,
46
+ fed_manifest_get: 5,
47
+ fed_manifest_data: 5,
46
48
  fed_partition_bridge: 5,
47
49
  fed_volatile: 10,
48
50
  })
package/node/identity.mjs CHANGED
@@ -1,8 +1,9 @@
1
+ import { Buffer } from 'node:buffer'
1
2
  import { randomBytes } from 'node:crypto'
2
3
 
3
- import { entityHashFromRecoveryPubKeyHex } from '../core/entity_id.mjs'
4
+ import { entityHashFromRecoveryPubKeyHex, parseEntityHash } from '../core/entity_id.mjs'
4
5
  import { isHex64 } from '../core/hexIds.mjs'
5
- import { nodeHashFromSeed } from '../entity/node_hash.mjs'
6
+ import { keyPairFromSeed, pubKeyHash } from '../crypto/crypto.mjs'
6
7
  import { normalizeMailboxSettings } from '../mailbox/settings.mjs'
7
8
 
8
9
  import { emitNodeChange } from './instance.mjs'
@@ -11,6 +12,18 @@ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
11
12
  const NODE_SEED_HEX_RE = /^[\da-f]{64}$/iu
12
13
  const NODE_JSON = 'node'
13
14
 
15
+ /**
16
+ * 由持久化 nodeSeed 派生 nodeHash(64 hex)。
17
+ * @param {string} seedHex 32 字节 hex
18
+ * @returns {string} 节点哈希
19
+ */
20
+ export function nodeHashFromSeed(seedHex) {
21
+ const seed = Buffer.from(String(seedHex).trim(), 'hex')
22
+ if (seed.length !== 32) throw new Error('invalid node seed')
23
+ const { publicKey } = keyPairFromSeed(seed)
24
+ return pubKeyHash(publicKey)
25
+ }
26
+
14
27
  /**
15
28
  * @returns {object} 节点配置磁盘对象
16
29
  */
@@ -97,3 +110,21 @@ export function entityHashFromKeys(nodeHash, recoveryPubKeyHex) {
97
110
  if (!isHex64(nodeHash) || !isHex64(pub)) return null
98
111
  return entityHashFromRecoveryPubKeyHex(nodeHash, pub)
99
112
  }
113
+
114
+ /**
115
+ * @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥
116
+ * @returns {string | null} 本节点 entityHash
117
+ */
118
+ export function resolveLocalEntityHashFromRecoveryPubKeyHex(recoveryPubKeyHex) {
119
+ return entityHashFromKeys(getNodeHash(), recoveryPubKeyHex)
120
+ }
121
+
122
+ /**
123
+ * @param {string} entityHash 目标 entityHash
124
+ * @returns {boolean} 是否为本节点可写实体
125
+ */
126
+ export function isWritableLocalEntity(entityHash) {
127
+ const parsed = parseEntityHash(entityHash)
128
+ if (!parsed) return false
129
+ return parsed.nodeHash === getNodeHash()
130
+ }
@@ -5,8 +5,8 @@
5
5
  */
6
6
  import { parseEntityHash } from '../core/entity_id.mjs'
7
7
  import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
8
- import { isWritableLocalEntity } from '../entity/replica.mjs'
9
8
 
9
+ import { isWritableLocalEntity } from './identity.mjs'
10
10
  import { isNodeInitialized, getEntityStore } from './instance.mjs'
11
11
 
12
12
  /** @typedef {'subject' | 'entity'} PersonalListScope */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -45,8 +45,6 @@
45
45
  "./rooms/*": "./rooms/*.mjs",
46
46
  "./overlay": "./overlay/index.mjs",
47
47
  "./overlay/*": "./overlay/*.mjs",
48
- "./entity/*": "./entity/*.mjs",
49
- "./entity/files/*": "./entity/files/*.mjs",
50
48
  "./trust_graph/*": "./trust_graph/*.mjs",
51
49
  "./mailbox/*": "./mailbox/*.mjs",
52
50
  "./dag": "./dag/index.mjs",
@@ -159,6 +159,8 @@ const NON_CRITICAL_FED_ACTIONS = new Set([
159
159
  'fed_chunk_get',
160
160
  'fed_chunk_data',
161
161
  'fed_chunk_ack',
162
+ 'fed_manifest_get',
163
+ 'fed_manifest_data',
162
164
  'part_invoke',
163
165
  'discovery_announce',
164
166
  'discovery_query',
@@ -1,46 +0,0 @@
1
- import { getEntityStore } from '../../node/instance.mjs'
2
-
3
- /** @type {Map<string, { replica: string | null, at: number }>} */
4
- const replicaHostCache = new Map()
5
-
6
- const REPLICA_HOST_CACHE_MS = 60_000
7
-
8
- /**
9
- * @param {string | string[]} [entityHash] owner(单个或批量)
10
- * @returns {void}
11
- */
12
- export function invalidateReplicaHostCache(entityHash) {
13
- if (Array.isArray(entityHash)) {
14
- for (const eh of entityHash)
15
- replicaHostCache.delete(String(eh).trim().toLowerCase())
16
- return
17
- }
18
- if (entityHash) {
19
- replicaHostCache.delete(String(entityHash).trim().toLowerCase())
20
- return
21
- }
22
- replicaHostCache.clear()
23
- }
24
-
25
- /**
26
- * @param {string} ownerEntityHash 所有者
27
- * @returns {Promise<string | null>} 托管 manifest 的 replica
28
- */
29
- export async function findReplicaHostingEntityFiles(ownerEntityHash) {
30
- const eh = String(ownerEntityHash).trim().toLowerCase()
31
- const cached = replicaHostCache.get(eh)
32
- if (cached && Date.now() - cached.at < REPLICA_HOST_CACHE_MS)
33
- return cached.replica
34
-
35
- const store = getEntityStore()
36
- if (typeof store.findHostingUser === 'function') {
37
- const replica = await store.findHostingUser(eh)
38
- replicaHostCache.set(eh, { replica, at: Date.now() })
39
- return replica
40
- }
41
-
42
- const files = await store.listEntityFiles(eh)
43
- const replica = files.length ? '__node__' : null
44
- replicaHostCache.set(eh, { replica, at: Date.now() })
45
- return replica
46
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * EVFS 文件 URL 辅助。
3
- * @param {string} entityHash 128 位十六进制
4
- * @param {string} logicalPath EVFS 逻辑路径
5
- * @returns {string} HTTP 地址
6
- */
7
- export function entityFileUrl(entityHash, logicalPath) {
8
- const path = String(logicalPath || '').trim().replace(/^\/+/, '')
9
- return `/api/p2p/entities/${encodeURIComponent(entityHash)}/files/${path.split('/').map(encodeURIComponent).join('/')}`
10
- }
11
-
12
- /**
13
- * profile 头像 EVFS 路径 URL。
14
- * @param {string} entityHash 128 位十六进制
15
- * @returns {string} profile 头像 HTTP 地址
16
- */
17
- export function profileAvatarFileUrl(entityHash) {
18
- return entityFileUrl(entityHash, 'profile/avatar')
19
- }
@@ -1,47 +0,0 @@
1
- /** @type {((username: string, entityHash: string) => string | null) | null} */
2
- let resolveAgentCharPartName = null
3
-
4
- /** @type {((username: string) => { entityHash: string, charPartName: string }[]) | null} */
5
- let listLocalAgents = null
6
-
7
- /**
8
- * Chat Load 时注册:解析本地 agent 的 chars 目录名。
9
- * @param {(username: string, entityHash: string) => string | null} resolver 解析函数
10
- * @returns {void}
11
- */
12
- export function registerAgentCharResolver(resolver) {
13
- resolveAgentCharPartName = resolver
14
- }
15
-
16
- /**
17
- * Chat Load 时注册:枚举 replica 下本地 agent 实体。
18
- * @param {(username: string) => { entityHash: string, charPartName: string }[]} provider 枚举函数
19
- * @returns {void}
20
- */
21
- export function registerListLocalAgentsProvider(provider) {
22
- listLocalAgents = provider
23
- }
24
-
25
- /** @returns {void} */
26
- export function unregisterAgentCharResolver() {
27
- resolveAgentCharPartName = null
28
- }
29
-
30
- /** @returns {void} */
31
- export function unregisterListLocalAgentsProvider() {
32
- listLocalAgents = null
33
- }
34
-
35
- /**
36
- * @returns {((username: string, entityHash: string) => string | null) | null} 已注册解析器
37
- */
38
- export function getAgentCharResolver() {
39
- return resolveAgentCharPartName
40
- }
41
-
42
- /**
43
- * @returns {((username: string) => { entityHash: string, charPartName: string }[]) | null} 已注册枚举器
44
- */
45
- export function getListLocalAgentsProvider() {
46
- return listLocalAgents
47
- }
@@ -1,141 +0,0 @@
1
- import { isEntityHash128 } from '../core/entity_id.mjs'
2
-
3
- import { profileAvatarFileUrl } from './files/url.mjs'
4
-
5
- /**
6
- * @param {unknown} linkItem 链接项
7
- * @returns {object | null} 规范化链接项或 null
8
- */
9
- function normalizeLink(linkItem) {
10
- const url = String(linkItem?.url || '').trim()
11
- if (!url) return null
12
- return {
13
- icon: String(linkItem.icon || '').trim(),
14
- name: String(linkItem.name || '').trim(),
15
- url,
16
- }
17
- }
18
-
19
- /**
20
- * @param {unknown} localizedInput 原始 localized 字段
21
- * @returns {Record<string, object>} locale → 切片
22
- */
23
- export function normalizeLocalizedMap(localizedInput) {
24
- if (!localizedInput) return {}
25
- /** @type {Record<string, object>} */
26
- const out = {}
27
- for (const [key, value] of Object.entries(localizedInput)) {
28
- const localeKey = String(key || '').trim()
29
- if (!localeKey || !value) continue
30
- const tags = Array.isArray(value.tags)
31
- ? value.tags.map(t => String(t).trim()).filter(Boolean)
32
- : undefined
33
- const links = Array.isArray(value.links)
34
- ? value.links.map(normalizeLink).filter(Boolean)
35
- : undefined
36
- /** @type {Record<string, unknown>} */
37
- const slice = {}
38
- if (value.name != null) slice.name = String(value.name).trim()
39
- if (value.avatar) slice.avatar = String(value.avatar).trim()
40
- if (value.description != null) slice.description = String(value.description)
41
- if (value.description_markdown != null) slice.description_markdown = String(value.description_markdown)
42
- if (value.version) slice.version = String(value.version).trim()
43
- if (value.author) slice.author = String(value.author).trim()
44
- if (value.home_page) slice.home_page = String(value.home_page).trim()
45
- if (value.issue_page) slice.issue_page = String(value.issue_page).trim()
46
- if (tags?.length) slice.tags = tags
47
- if (links?.length) slice.links = links
48
- if (Object.keys(slice).length) out[localeKey] = slice
49
- }
50
- return out
51
- }
52
-
53
- /**
54
- * @param {Record<string, object>} localized 多语言切片
55
- * @param {string[]} locales 区域设置优先级
56
- * @returns {object | null} 匹配的语言切片
57
- */
58
- function pickLocalizedSlice(localized, locales) {
59
- const keys = Object.keys(localized || {})
60
- if (!keys.length) return null
61
- for (const locale of locales || []) {
62
- if (localized[locale]) return localized[locale]
63
- const prefix = String(locale).split('-')[0]
64
- const hit = keys.find(k => k === prefix || k.startsWith(`${prefix}-`))
65
- if (hit) return localized[hit]
66
- }
67
- return localized[keys[0]]
68
- }
69
-
70
- /**
71
- * @param {string} displayName 展示名
72
- * @param {{ subjectHash?: string }} profile 资料对象
73
- * @returns {boolean} 是否为占位展示名
74
- */
75
- export function isPlaceholderDisplayName(displayName, profile) {
76
- const name = String(displayName || '').trim()
77
- if (!name) return true
78
- const subjectHash = String(profile?.subjectHash || '').trim().toLowerCase()
79
- if (!subjectHash || subjectHash.length < 12) return false
80
- const placeholder = `${subjectHash.slice(0, 8)}…${subjectHash.slice(-4)}`
81
- return name === placeholder
82
- }
83
-
84
- /**
85
- * @param {object} stored 磁盘上的 profile 对象
86
- * @param {string[]} locales 查看者区域设置
87
- * @param {object} infoDefaults part 默认
88
- * @returns {object} 合并后的展示字段
89
- */
90
- export function resolveProfilePresentation(stored, locales, infoDefaults) {
91
- const localized = normalizeLocalizedMap(stored?.localized)
92
- const slice = pickLocalizedSlice(localized, locales) || {}
93
-
94
- let name = slice.name?.trim() || infoDefaults.name
95
- if (name && isPlaceholderDisplayName(name, stored))
96
- name = infoDefaults.name
97
-
98
- const description = slice.description != null
99
- ? String(slice.description)
100
- : infoDefaults.description
101
- const description_markdown = slice.description_markdown != null
102
- ? String(slice.description_markdown)
103
- : slice.description != null ? String(slice.description) : infoDefaults.description_markdown
104
-
105
- const tags = slice.tags?.length ? slice.tags : infoDefaults.tags
106
- const links = slice.links?.length ? slice.links : infoDefaults.links
107
-
108
- let avatar = slice.avatar?.trim() || infoDefaults.avatar
109
- if (avatar && !avatar.startsWith('http') && isEntityHash128(stored?.entityHash))
110
- avatar = profileAvatarFileUrl(stored.entityHash)
111
- else if (!avatar && isEntityHash128(stored?.entityHash) && stored?.localized)
112
- avatar = profileAvatarFileUrl(stored.entityHash)
113
-
114
- return {
115
- name: name || infoDefaults.name,
116
- avatar: avatar || '',
117
- description: description || '',
118
- description_markdown: description_markdown || '',
119
- version: slice.version?.trim() || infoDefaults.version || '',
120
- author: slice.author?.trim() || infoDefaults.author || '',
121
- home_page: slice.home_page?.trim() || infoDefaults.home_page || '',
122
- issue_page: slice.issue_page?.trim() || infoDefaults.issue_page || '',
123
- tags: [...tags],
124
- links: [...links],
125
- }
126
- }
127
-
128
- /**
129
- * @param {Record<string, object>} localized 多语言表
130
- * @param {string} avatarUrl 头像 URL
131
- * @returns {Record<string, object>} 带头像 URL 的多语言表
132
- */
133
- export function applyAvatarToAllLocales(localized, avatarUrl) {
134
- const keys = Object.keys(localized)
135
- if (!keys.length) return { '': { avatar: avatarUrl } }
136
- /** @type {Record<string, object>} */
137
- const out = {}
138
- for (const key of keys)
139
- out[key] = { ...localized[key], avatar: avatarUrl }
140
- return out
141
- }
@@ -1,40 +0,0 @@
1
- /** @type {Map<string, (username: string, entityHash: string) => Promise<string | null>>} */
2
- const resolversByOwner = new Map()
3
-
4
- /**
5
- * @param {string} ownerId 注册方
6
- * @param {(username: string, entityHash: string) => Promise<string | null>} resolver logical entity id 反查
7
- * @returns {void}
8
- */
9
- export function registerLogicalEntityIdResolver(ownerId, resolver) {
10
- resolversByOwner.set(String(ownerId), resolver)
11
- }
12
-
13
- /**
14
- * @param {string} ownerId 注册方
15
- * @returns {void}
16
- */
17
- export function unregisterLogicalEntityIdResolver(ownerId) {
18
- resolversByOwner.delete(String(ownerId))
19
- }
20
-
21
- /** @returns {void} */
22
- export function clearLogicalEntityIdRegistry() {
23
- resolversByOwner.clear()
24
- }
25
-
26
- /**
27
- * @param {string} username 副本用户名
28
- * @param {string} entityHash 128 位十六进制 logical entity
29
- * @returns {Promise<string | null>} 逻辑实体 id
30
- */
31
- export async function resolveLogicalEntityId(username, entityHash) {
32
- for (const resolver of resolversByOwner.values())
33
- try {
34
- const id = await resolver(username, entityHash)
35
- if (id) return id
36
- }
37
- catch { /* next */ }
38
-
39
- return null
40
- }
@@ -1,15 +0,0 @@
1
- import { Buffer } from 'node:buffer'
2
-
3
- import { keyPairFromSeed, pubKeyHash } from '../crypto/crypto.mjs'
4
-
5
- /**
6
- * 由持久化 nodeSeed 派生 nodeHash(64 hex)。
7
- * @param {string} seedHex 32 字节 hex
8
- * @returns {string} 节点哈希
9
- */
10
- export function nodeHashFromSeed(seedHex) {
11
- const seed = Buffer.from(String(seedHex).trim(), 'hex')
12
- if (seed.length !== 32) throw new Error('invalid node seed')
13
- const { publicKey } = keyPairFromSeed(seed)
14
- return pubKeyHash(publicKey)
15
- }
@@ -1,44 +0,0 @@
1
- /** @typedef {{ query?: { locales?: string | string[] } }} ExpressLikeRequest */
2
-
3
- /** @type {((replicaUsername: string, entityHash: string, locales: string[]) => Promise<object>) | null} */
4
- let infoDefaultsProvider = null
5
-
6
- /** @type {((req: ExpressLikeRequest, replicaUsername: string) => string[]) | null} */
7
- let localesFromRequestProvider = null
8
-
9
- /**
10
- * @param {(replicaUsername: string, entityHash: string, locales: string[]) => Promise<object>} provider 资料默认值解析器
11
- * @returns {void}
12
- */
13
- export function registerEntityPresentationProvider(provider) {
14
- infoDefaultsProvider = provider
15
- }
16
-
17
- /**
18
- * @param {(req: ExpressLikeRequest, replicaUsername: string) => string[]} provider 请求区域设置解析器
19
- * @returns {void}
20
- */
21
- export function registerLocalesFromRequestProvider(provider) {
22
- localesFromRequestProvider = provider
23
- }
24
-
25
- /**
26
- * @param {string} replicaUsername 查看者 replica
27
- * @param {string} entityHash 目标实体
28
- * @param {string[]} locales 区域设置优先级
29
- * @returns {Promise<object | null>} 展示默认值或 null
30
- */
31
- export async function resolveInfoDefaultsForEntity(replicaUsername, entityHash, locales) {
32
- if (!infoDefaultsProvider) return null
33
- return infoDefaultsProvider(replicaUsername, entityHash, locales)
34
- }
35
-
36
- /**
37
- * @param {ExpressLikeRequest} req HTTP 请求
38
- * @param {string} replicaUsername 查看者 replica
39
- * @returns {string[]} 区域设置列表
40
- */
41
- export function localesFromRequest(req, replicaUsername) {
42
- if (localesFromRequestProvider) return localesFromRequestProvider(req, replicaUsername)
43
- return ['zh-CN', 'en-UK']
44
- }