@antseed/node 0.1.0 → 0.1.1

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 (130) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/interfaces/seller-provider.d.ts +13 -1
  6. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  7. package/dist/node.d.ts +13 -3
  8. package/dist/node.d.ts.map +1 -1
  9. package/dist/node.js +123 -15
  10. package/dist/node.js.map +1 -1
  11. package/dist/proxy/proxy-mux.d.ts +3 -1
  12. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  13. package/dist/proxy/proxy-mux.js +9 -5
  14. package/dist/proxy/proxy-mux.js.map +1 -1
  15. package/dist/types/http.d.ts +1 -0
  16. package/dist/types/http.d.ts.map +1 -1
  17. package/dist/types/http.js +1 -1
  18. package/dist/types/http.js.map +1 -1
  19. package/package.json +14 -10
  20. package/contracts/AntseedEscrow.sol +0 -310
  21. package/contracts/MockUSDC.sol +0 -64
  22. package/contracts/README.md +0 -102
  23. package/src/config/encryption.test.ts +0 -49
  24. package/src/config/encryption.ts +0 -53
  25. package/src/config/plugin-config-manager.test.ts +0 -92
  26. package/src/config/plugin-config-manager.ts +0 -153
  27. package/src/config/plugin-loader.ts +0 -90
  28. package/src/discovery/announcer.ts +0 -169
  29. package/src/discovery/bootstrap.ts +0 -57
  30. package/src/discovery/default-metadata-resolver.ts +0 -18
  31. package/src/discovery/dht-health.ts +0 -136
  32. package/src/discovery/dht-node.ts +0 -191
  33. package/src/discovery/http-metadata-resolver.ts +0 -47
  34. package/src/discovery/index.ts +0 -15
  35. package/src/discovery/metadata-codec.ts +0 -453
  36. package/src/discovery/metadata-resolver.ts +0 -7
  37. package/src/discovery/metadata-server.ts +0 -73
  38. package/src/discovery/metadata-validator.ts +0 -172
  39. package/src/discovery/peer-lookup.ts +0 -122
  40. package/src/discovery/peer-metadata.ts +0 -34
  41. package/src/discovery/peer-selector.ts +0 -134
  42. package/src/discovery/profile-manager.ts +0 -131
  43. package/src/discovery/profile-search.ts +0 -100
  44. package/src/discovery/reputation-verifier.ts +0 -54
  45. package/src/index.ts +0 -61
  46. package/src/interfaces/buyer-router.ts +0 -21
  47. package/src/interfaces/plugin.ts +0 -36
  48. package/src/interfaces/seller-provider.ts +0 -81
  49. package/src/metering/index.ts +0 -6
  50. package/src/metering/receipt-generator.ts +0 -105
  51. package/src/metering/receipt-verifier.ts +0 -102
  52. package/src/metering/session-tracker.ts +0 -145
  53. package/src/metering/storage.ts +0 -600
  54. package/src/metering/token-counter.ts +0 -127
  55. package/src/metering/usage-aggregator.ts +0 -236
  56. package/src/node.ts +0 -1698
  57. package/src/p2p/connection-auth.ts +0 -152
  58. package/src/p2p/connection-manager.ts +0 -916
  59. package/src/p2p/handshake.ts +0 -162
  60. package/src/p2p/ice-config.ts +0 -59
  61. package/src/p2p/identity.ts +0 -110
  62. package/src/p2p/index.ts +0 -11
  63. package/src/p2p/keepalive.ts +0 -118
  64. package/src/p2p/message-protocol.ts +0 -171
  65. package/src/p2p/nat-traversal.ts +0 -169
  66. package/src/p2p/payment-codec.ts +0 -165
  67. package/src/p2p/payment-mux.ts +0 -153
  68. package/src/p2p/reconnect.ts +0 -117
  69. package/src/payments/balance-manager.ts +0 -77
  70. package/src/payments/buyer-payment-manager.ts +0 -414
  71. package/src/payments/disputes.ts +0 -72
  72. package/src/payments/evm/escrow-client.ts +0 -263
  73. package/src/payments/evm/keypair.ts +0 -31
  74. package/src/payments/evm/signatures.ts +0 -103
  75. package/src/payments/evm/wallet.ts +0 -42
  76. package/src/payments/index.ts +0 -50
  77. package/src/payments/settlement.ts +0 -40
  78. package/src/payments/types.ts +0 -79
  79. package/src/proxy/index.ts +0 -3
  80. package/src/proxy/provider-detection.ts +0 -78
  81. package/src/proxy/proxy-mux.ts +0 -173
  82. package/src/proxy/request-codec.ts +0 -294
  83. package/src/reputation/index.ts +0 -6
  84. package/src/reputation/rating-manager.ts +0 -118
  85. package/src/reputation/report-manager.ts +0 -91
  86. package/src/reputation/trust-engine.ts +0 -120
  87. package/src/reputation/trust-score.ts +0 -74
  88. package/src/reputation/uptime-tracker.ts +0 -155
  89. package/src/routing/default-router.ts +0 -75
  90. package/src/types/bittorrent-dht.d.ts +0 -19
  91. package/src/types/buyer.ts +0 -37
  92. package/src/types/capability.ts +0 -34
  93. package/src/types/connection.ts +0 -29
  94. package/src/types/http.ts +0 -20
  95. package/src/types/index.ts +0 -14
  96. package/src/types/metering.ts +0 -175
  97. package/src/types/nat-api.d.ts +0 -29
  98. package/src/types/peer-profile.ts +0 -25
  99. package/src/types/peer.ts +0 -62
  100. package/src/types/plugin-config.ts +0 -31
  101. package/src/types/protocol.ts +0 -162
  102. package/src/types/provider.ts +0 -40
  103. package/src/types/rating.ts +0 -23
  104. package/src/types/report.ts +0 -30
  105. package/src/types/seller.ts +0 -38
  106. package/src/types/staking.ts +0 -23
  107. package/src/utils/debug.ts +0 -30
  108. package/src/utils/hex.ts +0 -14
  109. package/tests/balance-manager.test.ts +0 -156
  110. package/tests/bootstrap.test.ts +0 -108
  111. package/tests/buyer-payment-manager.test.ts +0 -358
  112. package/tests/connection-auth.test.ts +0 -87
  113. package/tests/default-router.test.ts +0 -148
  114. package/tests/evm-keypair.test.ts +0 -173
  115. package/tests/identity.test.ts +0 -133
  116. package/tests/message-protocol.test.ts +0 -212
  117. package/tests/metadata-codec.test.ts +0 -165
  118. package/tests/metadata-validator.test.ts +0 -261
  119. package/tests/metering-storage.test.ts +0 -244
  120. package/tests/payment-codec.test.ts +0 -95
  121. package/tests/payment-mux.test.ts +0 -191
  122. package/tests/peer-selector.test.ts +0 -184
  123. package/tests/provider-detection.test.ts +0 -107
  124. package/tests/proxy-mux-security.test.ts +0 -38
  125. package/tests/receipt.test.ts +0 -215
  126. package/tests/reputation-integration.test.ts +0 -195
  127. package/tests/request-codec.test.ts +0 -144
  128. package/tests/token-counter.test.ts +0 -122
  129. package/tsconfig.json +0 -9
  130. package/vitest.config.ts +0 -7
@@ -1,53 +0,0 @@
1
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto'
2
- import { hostname, homedir } from 'node:os'
3
-
4
- const ALGORITHM = 'aes-256-gcm'
5
- const SALT_LENGTH = 32
6
- const IV_LENGTH = 16
7
- const TAG_LENGTH = 16
8
- const KEY_LENGTH = 32
9
-
10
- /**
11
- * Derive an encryption key from a machine-specific seed.
12
- */
13
- export function deriveMachineKey(salt: Buffer): Buffer {
14
- const seed = [
15
- process.env['USER'] ?? process.env['USERNAME'] ?? '',
16
- hostname(),
17
- homedir(),
18
- ].join(':')
19
- return scryptSync(seed, salt, KEY_LENGTH)
20
- }
21
-
22
- /**
23
- * Encrypt a string value. Returns base64-encoded ciphertext with IV and auth tag prepended.
24
- */
25
- export function encryptValue(plaintext: string, key: Buffer): string {
26
- const iv = randomBytes(IV_LENGTH)
27
- const cipher = createCipheriv(ALGORITHM, key, iv)
28
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()])
29
- const tag = cipher.getAuthTag()
30
- const combined = Buffer.concat([iv, tag, encrypted])
31
- return combined.toString('base64')
32
- }
33
-
34
- /**
35
- * Decrypt a base64-encoded value produced by encryptValue.
36
- */
37
- export function decryptValue(encoded: string, key: Buffer): string {
38
- const combined = Buffer.from(encoded, 'base64')
39
- const iv = combined.subarray(0, IV_LENGTH)
40
- const tag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH)
41
- const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH)
42
- const decipher = createDecipheriv(ALGORITHM, key, iv)
43
- decipher.setAuthTag(tag)
44
- const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
45
- return decrypted.toString('utf-8')
46
- }
47
-
48
- /**
49
- * Generate a fresh random salt for key derivation.
50
- */
51
- export function generateSalt(): Buffer {
52
- return randomBytes(SALT_LENGTH)
53
- }
@@ -1,92 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { mkdtemp, rm } from 'node:fs/promises'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { addInstance, removeInstance, getInstance, getInstances, updateInstanceConfig, loadPluginConfig } from './plugin-config-manager.js'
6
- import type { ConfigField } from '../interfaces/plugin.js'
7
-
8
- const schema: ConfigField[] = [
9
- { key: 'API_KEY', label: 'API Key', type: 'secret', required: true },
10
- { key: 'BASE_URL', label: 'Base URL', type: 'string', required: false },
11
- ]
12
-
13
- describe('plugin-config-manager', () => {
14
- let tmpDir: string
15
- let configPath: string
16
-
17
- beforeEach(async () => {
18
- tmpDir = await mkdtemp(join(tmpdir(), 'antseed-test-'))
19
- configPath = join(tmpDir, 'config.json')
20
- })
21
-
22
- it('should add and retrieve an instance', async () => {
23
- await addInstance(configPath, {
24
- id: 'test-provider',
25
- package: '@antseed/provider-test',
26
- type: 'provider',
27
- config: { API_KEY: 'sk-secret-123', BASE_URL: 'https://api.test.com' },
28
- }, schema)
29
-
30
- const instance = await getInstance(configPath, 'test-provider')
31
- expect(instance).not.toBeNull()
32
- expect(instance!.id).toBe('test-provider')
33
- expect(instance!.config['API_KEY']).toBe('sk-secret-123')
34
- expect(instance!.config['BASE_URL']).toBe('https://api.test.com')
35
- })
36
-
37
- it('should encrypt secrets in the file', async () => {
38
- await addInstance(configPath, {
39
- id: 'test',
40
- package: '@antseed/provider-test',
41
- type: 'provider',
42
- config: { API_KEY: 'sk-secret', BASE_URL: 'https://api.test.com' },
43
- }, schema)
44
-
45
- const raw = await loadPluginConfig(configPath)
46
- const stored = raw.instances[0]!.config
47
- // Secret should be encrypted (prefixed with enc:)
48
- expect(typeof stored['API_KEY']).toBe('string')
49
- expect((stored['API_KEY'] as string).startsWith('enc:')).toBe(true)
50
- // Non-secret should be plain
51
- expect(stored['BASE_URL']).toBe('https://api.test.com')
52
- })
53
-
54
- it('should remove an instance', async () => {
55
- await addInstance(configPath, {
56
- id: 'to-remove',
57
- package: '@antseed/provider-test',
58
- type: 'provider',
59
- config: {},
60
- })
61
- await removeInstance(configPath, 'to-remove')
62
- const instance = await getInstance(configPath, 'to-remove')
63
- expect(instance).toBeNull()
64
- })
65
-
66
- it('should list all instances', async () => {
67
- await addInstance(configPath, { id: 'a', package: 'pkg-a', type: 'provider', config: {} })
68
- await addInstance(configPath, { id: 'b', package: 'pkg-b', type: 'router', config: {} })
69
- const all = await getInstances(configPath)
70
- expect(all).toHaveLength(2)
71
- expect(all.map(i => i.id)).toEqual(['a', 'b'])
72
- })
73
-
74
- it('should update instance config', async () => {
75
- await addInstance(configPath, {
76
- id: 'updatable',
77
- package: 'pkg',
78
- type: 'provider',
79
- config: { API_KEY: 'old-key' },
80
- }, schema)
81
-
82
- await updateInstanceConfig(configPath, 'updatable', { API_KEY: 'new-key' }, schema)
83
- const updated = await getInstance(configPath, 'updatable')
84
- expect(updated!.config['API_KEY']).toBe('new-key')
85
- })
86
-
87
- it('should reject duplicate instance IDs', async () => {
88
- await addInstance(configPath, { id: 'dup', package: 'pkg', type: 'provider', config: {} })
89
- await expect(addInstance(configPath, { id: 'dup', package: 'pkg', type: 'provider', config: {} }))
90
- .rejects.toThrow('already exists')
91
- })
92
- })
@@ -1,153 +0,0 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises'
2
- import { dirname } from 'node:path'
3
- import type { PluginInstanceConfig, PluginConfigFile } from '../types/plugin-config.js'
4
- import type { ConfigField } from '../interfaces/plugin.js'
5
- import { encryptValue, decryptValue, deriveMachineKey, generateSalt } from './encryption.js'
6
-
7
- const ENCRYPTED_PREFIX = 'enc:'
8
-
9
- function isEncrypted(value: unknown): value is string {
10
- return typeof value === 'string' && value.startsWith(ENCRYPTED_PREFIX)
11
- }
12
-
13
- /**
14
- * Load plugin config file. Returns empty config if file doesn't exist.
15
- */
16
- export async function loadPluginConfig(configPath: string): Promise<PluginConfigFile> {
17
- try {
18
- const raw = await readFile(configPath, 'utf-8')
19
- const parsed = JSON.parse(raw) as Record<string, unknown>
20
- return {
21
- instances: Array.isArray(parsed['instances']) ? parsed['instances'] as PluginInstanceConfig[] : [],
22
- encryption: parsed['encryption'] as PluginConfigFile['encryption'],
23
- }
24
- } catch (err) {
25
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
26
- return { instances: [] }
27
- }
28
- throw err
29
- }
30
- }
31
-
32
- /**
33
- * Save plugin config file. Preserves non-plugin fields.
34
- */
35
- export async function savePluginConfig(configPath: string, pluginConfig: PluginConfigFile): Promise<void> {
36
- let existing: Record<string, unknown> = {}
37
- try {
38
- const raw = await readFile(configPath, 'utf-8')
39
- existing = JSON.parse(raw) as Record<string, unknown>
40
- } catch {
41
- // File doesn't exist yet
42
- }
43
-
44
- const merged = {
45
- ...existing,
46
- instances: pluginConfig.instances,
47
- ...(pluginConfig.encryption ? { encryption: pluginConfig.encryption } : {}),
48
- }
49
-
50
- await mkdir(dirname(configPath), { recursive: true })
51
- await writeFile(configPath, JSON.stringify(merged, null, 2), 'utf-8')
52
- }
53
-
54
- function getOrCreateEncryption(config: PluginConfigFile): { key: Buffer; encryption: NonNullable<PluginConfigFile['encryption']> } {
55
- if (config.encryption?.salt) {
56
- const salt = Buffer.from(config.encryption.salt, 'hex')
57
- return { key: deriveMachineKey(salt), encryption: config.encryption }
58
- }
59
- const salt = generateSalt()
60
- const encryption = { algorithm: 'aes-256-gcm', kdf: 'scrypt', salt: salt.toString('hex') }
61
- return { key: deriveMachineKey(salt), encryption }
62
- }
63
-
64
- function encryptSecrets(instanceConfig: Record<string, unknown>, key: Buffer, schema?: ConfigField[]): Record<string, unknown> {
65
- const result = { ...instanceConfig }
66
- const secretKeys = new Set(schema?.filter(f => f.type === 'secret').map(f => f.key) ?? [])
67
- for (const [k, v] of Object.entries(result)) {
68
- if (secretKeys.has(k) && typeof v === 'string' && !isEncrypted(v)) {
69
- result[k] = ENCRYPTED_PREFIX + encryptValue(v, key)
70
- }
71
- }
72
- return result
73
- }
74
-
75
- function decryptSecrets(instanceConfig: Record<string, unknown>, key: Buffer): Record<string, unknown> {
76
- const result = { ...instanceConfig }
77
- for (const [k, v] of Object.entries(result)) {
78
- if (isEncrypted(v)) {
79
- result[k] = decryptValue(v.slice(ENCRYPTED_PREFIX.length), key)
80
- }
81
- }
82
- return result
83
- }
84
-
85
- /**
86
- * Add a plugin instance with encrypted secrets.
87
- */
88
- export async function addInstance(
89
- configPath: string,
90
- instance: PluginInstanceConfig,
91
- schema?: ConfigField[],
92
- ): Promise<void> {
93
- const config = await loadPluginConfig(configPath)
94
- if (config.instances.some(i => i.id === instance.id)) {
95
- throw new Error(`Instance "${instance.id}" already exists`)
96
- }
97
- const { key, encryption } = getOrCreateEncryption(config)
98
- config.encryption = encryption
99
- const encrypted = { ...instance, config: encryptSecrets(instance.config, key, schema) }
100
- config.instances.push(encrypted)
101
- await savePluginConfig(configPath, config)
102
- }
103
-
104
- /**
105
- * Remove an instance by ID.
106
- */
107
- export async function removeInstance(configPath: string, instanceId: string): Promise<void> {
108
- const config = await loadPluginConfig(configPath)
109
- config.instances = config.instances.filter(i => i.id !== instanceId)
110
- await savePluginConfig(configPath, config)
111
- }
112
-
113
- /**
114
- * Get a single instance with decrypted secrets.
115
- */
116
- export async function getInstance(configPath: string, instanceId: string): Promise<PluginInstanceConfig | null> {
117
- const config = await loadPluginConfig(configPath)
118
- const instance = config.instances.find(i => i.id === instanceId)
119
- if (!instance) return null
120
- if (!config.encryption?.salt) return instance
121
- const salt = Buffer.from(config.encryption.salt, 'hex')
122
- const key = deriveMachineKey(salt)
123
- return { ...instance, config: decryptSecrets(instance.config, key) }
124
- }
125
-
126
- /**
127
- * Get all instances with decrypted secrets.
128
- */
129
- export async function getInstances(configPath: string): Promise<PluginInstanceConfig[]> {
130
- const config = await loadPluginConfig(configPath)
131
- if (!config.encryption?.salt) return config.instances
132
- const salt = Buffer.from(config.encryption.salt, 'hex')
133
- const key = deriveMachineKey(salt)
134
- return config.instances.map(i => ({ ...i, config: decryptSecrets(i.config, key) }))
135
- }
136
-
137
- /**
138
- * Update config for an existing instance.
139
- */
140
- export async function updateInstanceConfig(
141
- configPath: string,
142
- instanceId: string,
143
- newConfig: Record<string, unknown>,
144
- schema?: ConfigField[],
145
- ): Promise<void> {
146
- const config = await loadPluginConfig(configPath)
147
- const idx = config.instances.findIndex(i => i.id === instanceId)
148
- if (idx === -1) throw new Error(`Instance "${instanceId}" not found`)
149
- const { key, encryption } = getOrCreateEncryption(config)
150
- config.encryption = encryption
151
- config.instances[idx] = { ...config.instances[idx]!, config: encryptSecrets(newConfig, key, schema) }
152
- await savePluginConfig(configPath, config)
153
- }
@@ -1,90 +0,0 @@
1
- import type { Provider } from '../interfaces/seller-provider.js'
2
- import type { Router } from '../interfaces/buyer-router.js'
3
- import type { AntseedProviderPlugin, AntseedRouterPlugin, AntseedPlugin } from '../interfaces/plugin.js'
4
- import { getInstances } from './plugin-config-manager.js'
5
-
6
- export interface LoadedProvider {
7
- instanceId: string
8
- provider: Provider
9
- pluginName: string
10
- }
11
-
12
- export interface LoadedRouter {
13
- instanceId: string
14
- router: Router
15
- pluginName: string
16
- }
17
-
18
- /**
19
- * Load a plugin's default export from its npm package.
20
- */
21
- export async function loadPluginModule(packageName: string, pluginsDir: string): Promise<AntseedPlugin> {
22
- const { join, resolve } = await import('node:path')
23
- const pluginPath = join(pluginsDir, 'node_modules', packageName, 'dist', 'index.js')
24
- const resolved = resolve(pluginPath)
25
-
26
- if (!resolved.startsWith(resolve(pluginsDir))) {
27
- throw new Error(`Invalid plugin path: ${packageName}`)
28
- }
29
-
30
- let mod: { default?: unknown }
31
- try {
32
- mod = await import(resolved) as { default?: unknown }
33
- } catch (err) {
34
- const cause = err instanceof Error ? err.message : String(err)
35
- throw new Error(`Plugin "${packageName}" failed to load from ${resolved}: ${cause}`)
36
- }
37
-
38
- const plugin = mod.default
39
- if (!plugin || typeof plugin !== 'object') {
40
- throw new Error(`Plugin "${packageName}" does not export a valid plugin object`)
41
- }
42
-
43
- const typed = plugin as { type?: string }
44
- if (typed.type !== 'provider' && typed.type !== 'router') {
45
- throw new Error(`Plugin "${packageName}" has invalid type: ${typed.type}`)
46
- }
47
-
48
- return plugin as AntseedPlugin
49
- }
50
-
51
- function instanceConfigToRecord(config: Record<string, unknown>): Record<string, string> {
52
- const result: Record<string, string> = {}
53
- for (const [key, value] of Object.entries(config)) {
54
- if (value !== null && value !== undefined) {
55
- result[key] = String(value)
56
- }
57
- }
58
- return result
59
- }
60
-
61
- /**
62
- * Load all enabled plugin instances from config and instantiate them.
63
- */
64
- export async function loadAllPlugins(configPath: string, pluginsDir: string): Promise<{
65
- providers: LoadedProvider[]
66
- routers: LoadedRouter[]
67
- }> {
68
- const instances = await getInstances(configPath)
69
- const providers: LoadedProvider[] = []
70
- const routers: LoadedRouter[] = []
71
-
72
- for (const instance of instances) {
73
- if (instance.enabled === false) continue
74
-
75
- const plugin = await loadPluginModule(instance.package, pluginsDir)
76
- const configRecord = instanceConfigToRecord(instance.config)
77
-
78
- if (instance.type === 'provider' && plugin.type === 'provider') {
79
- const providerPlugin = plugin as AntseedProviderPlugin
80
- const provider = await providerPlugin.createProvider(configRecord)
81
- providers.push({ instanceId: instance.id, provider, pluginName: plugin.name })
82
- } else if (instance.type === 'router' && plugin.type === 'router') {
83
- const routerPlugin = plugin as AntseedRouterPlugin
84
- const router = await routerPlugin.createRouter(configRecord)
85
- routers.push({ instanceId: instance.id, router, pluginName: plugin.name })
86
- }
87
- }
88
-
89
- return { providers, routers }
90
- }
@@ -1,169 +0,0 @@
1
- import type { Identity } from "../p2p/identity.js";
2
- import { signData } from "../p2p/identity.js";
3
- import type { DHTNode } from "./dht-node.js";
4
- import { providerTopic, capabilityTopic, topicToInfoHash } from "./dht-node.js";
5
- import type { PeerOffering } from "../types/capability.js";
6
- import type { PeerMetadata, ProviderAnnouncement } from "./peer-metadata.js";
7
- import { METADATA_VERSION } from "./peer-metadata.js";
8
- import { encodeMetadataForSigning } from "./metadata-codec.js";
9
- import { debugWarn } from "../utils/debug.js";
10
- import { bytesToHex } from "../utils/hex.js";
11
- import type { BaseEscrowClient } from "../payments/evm/escrow-client.js";
12
- import { identityToEvmAddress } from "../payments/evm/keypair.js";
13
-
14
- export interface AnnouncerConfig {
15
- identity: Identity;
16
- dht: DHTNode;
17
- providers: Array<{
18
- provider: string;
19
- models: string[];
20
- maxConcurrency: number;
21
- }>;
22
- region: string;
23
- pricing: Map<
24
- string,
25
- {
26
- defaults: { inputUsdPerMillion: number; outputUsdPerMillion: number };
27
- models?: Record<string, { inputUsdPerMillion: number; outputUsdPerMillion: number }>;
28
- }
29
- >;
30
- offerings?: PeerOffering[];
31
- stakeAmountUSDC?: number;
32
- trustScore?: number;
33
- escrowClient?: BaseEscrowClient;
34
- reannounceIntervalMs: number;
35
- signalingPort: number;
36
- }
37
-
38
- export class PeerAnnouncer {
39
- private readonly config: AnnouncerConfig;
40
- private intervalHandle: ReturnType<typeof setInterval> | null = null;
41
- private readonly loadMap: Map<string, number> = new Map();
42
- private _latestMetadata: PeerMetadata | null = null;
43
-
44
- constructor(config: AnnouncerConfig) {
45
- this.config = config;
46
- }
47
-
48
- async announce(): Promise<void> {
49
- const providers: ProviderAnnouncement[] = this.config.providers.map((p) => {
50
- const pricing = this.config.pricing.get(p.provider) ?? {
51
- defaults: {
52
- inputUsdPerMillion: 0,
53
- outputUsdPerMillion: 0,
54
- },
55
- };
56
- return {
57
- provider: p.provider,
58
- models: p.models,
59
- defaultPricing: pricing.defaults,
60
- ...(pricing.models ? { modelPricing: pricing.models } : {}),
61
- maxConcurrency: p.maxConcurrency,
62
- currentLoad: this.loadMap.get(p.provider) ?? 0,
63
- };
64
- });
65
-
66
- const metadata: PeerMetadata = {
67
- peerId: this.config.identity.peerId,
68
- version: METADATA_VERSION,
69
- providers,
70
- ...(this.config.offerings && this.config.offerings.length > 0
71
- ? { offerings: this.config.offerings }
72
- : {}),
73
- ...(this.config.stakeAmountUSDC != null
74
- ? { stakeAmountUSDC: this.config.stakeAmountUSDC }
75
- : {}),
76
- ...(this.config.trustScore != null
77
- ? { trustScore: this.config.trustScore }
78
- : {}),
79
- region: this.config.region,
80
- timestamp: Date.now(),
81
- signature: "",
82
- };
83
-
84
- // Populate EVM address and on-chain reputation if escrow client is available
85
- if (this.config.escrowClient) {
86
- try {
87
- const evmAddress = identityToEvmAddress(this.config.identity);
88
- metadata.evmAddress = evmAddress;
89
- const reputation = await this.config.escrowClient.getReputation(evmAddress);
90
- metadata.onChainReputation = reputation.weightedAverage;
91
- metadata.onChainSessionCount = reputation.sessionCount;
92
- metadata.onChainDisputeCount = reputation.disputeCount;
93
- } catch {
94
- // Silently continue without reputation data
95
- }
96
- }
97
-
98
- // Sign metadata
99
- const dataToSign = encodeMetadataForSigning(metadata);
100
- const signature = await signData(
101
- this.config.identity.privateKey,
102
- dataToSign
103
- );
104
- metadata.signature = bytesToHex(signature);
105
- this._latestMetadata = metadata;
106
-
107
- // Announce under each provider topic (continue on failure)
108
- for (const p of providers) {
109
- try {
110
- const topic = providerTopic(p.provider);
111
- const infoHash = topicToInfoHash(topic);
112
- await this.config.dht.announce(infoHash, this.config.signalingPort);
113
- } catch {
114
- // DHT may not have peers yet — will retry on next cycle
115
- }
116
- }
117
-
118
- // Also announce under the wildcard topic for generic discovery
119
- try {
120
- const wildcardInfoHash = topicToInfoHash(providerTopic("*"));
121
- await this.config.dht.announce(wildcardInfoHash, this.config.signalingPort);
122
- } catch {
123
- // DHT may not have peers yet — will retry on next cycle
124
- }
125
-
126
- // Announce under each capability topic
127
- if (this.config.offerings) {
128
- for (const offering of this.config.offerings) {
129
- try {
130
- const topic = capabilityTopic(offering.capability, offering.name);
131
- const infoHash = topicToInfoHash(topic);
132
- await this.config.dht.announce(infoHash, this.config.signalingPort);
133
- } catch {
134
- // DHT may not have peers yet — will retry on next cycle
135
- }
136
- }
137
- }
138
- }
139
-
140
- startPeriodicAnnounce(): void {
141
- if (this.intervalHandle) {
142
- return;
143
- }
144
- // Announce immediately, then on interval
145
- void this.announce().catch((err) => {
146
- debugWarn(`[Announcer] Initial announce failed: ${err instanceof Error ? err.message : err}`);
147
- });
148
- this.intervalHandle = setInterval(() => {
149
- void this.announce().catch((err) => {
150
- debugWarn(`[Announcer] Periodic announce failed: ${err instanceof Error ? err.message : err}`);
151
- });
152
- }, this.config.reannounceIntervalMs);
153
- }
154
-
155
- stopPeriodicAnnounce(): void {
156
- if (this.intervalHandle) {
157
- clearInterval(this.intervalHandle);
158
- this.intervalHandle = null;
159
- }
160
- }
161
-
162
- updateLoad(providerName: string, currentLoad: number): void {
163
- this.loadMap.set(providerName, currentLoad);
164
- }
165
-
166
- getLatestMetadata(): PeerMetadata | null {
167
- return this._latestMetadata;
168
- }
169
- }
@@ -1,57 +0,0 @@
1
- export interface BootstrapNode {
2
- host: string;
3
- port: number;
4
- label?: string;
5
- }
6
-
7
- export const OFFICIAL_BOOTSTRAP_NODES: BootstrapNode[] = [
8
- { host: "router.bittorrent.com", port: 6881, label: "BitTorrent" },
9
- { host: "dht.transmissionbt.com", port: 6881, label: "Transmission" },
10
- { host: "router.utorrent.com", port: 6881, label: "uTorrent" },
11
- { host: "dht.libtorrent.org", port: 25401, label: "libtorrent" },
12
- { host: "router.silotis.us", port: 6881, label: "Silotis" },
13
- { host: "dht.aelitis.com", port: 6881, label: "Vuze" },
14
- ];
15
-
16
- export function parseBootstrapList(entries: string[]): BootstrapNode[] {
17
- return entries.map((entry) => {
18
- const lastColon = entry.lastIndexOf(":");
19
- if (lastColon === -1) {
20
- throw new Error(`Invalid bootstrap entry, missing port: "${entry}"`);
21
- }
22
-
23
- const host = entry.slice(0, lastColon);
24
- const portStr = entry.slice(lastColon + 1);
25
- const port = parseInt(portStr, 10);
26
-
27
- if (!Number.isFinite(port) || port < 1 || port > 65535) {
28
- throw new Error(`Invalid port in bootstrap entry: "${entry}"`);
29
- }
30
-
31
- return { host, port };
32
- });
33
- }
34
-
35
- export function mergeBootstrapNodes(
36
- official: BootstrapNode[],
37
- userConfigured: BootstrapNode[]
38
- ): BootstrapNode[] {
39
- const seen = new Set<string>();
40
- const result: BootstrapNode[] = [];
41
-
42
- for (const node of [...official, ...userConfigured]) {
43
- const key = `${node.host}:${node.port}`;
44
- if (!seen.has(key)) {
45
- seen.add(key);
46
- result.push(node);
47
- }
48
- }
49
-
50
- return result;
51
- }
52
-
53
- export function toBootstrapConfig(
54
- nodes: BootstrapNode[]
55
- ): Array<{ host: string; port: number }> {
56
- return nodes.map((n) => ({ host: n.host, port: n.port }));
57
- }
@@ -1,18 +0,0 @@
1
- import type { PeerEndpoint, MetadataResolver } from "./metadata-resolver.js";
2
- import type { PeerMetadata } from "./peer-metadata.js";
3
-
4
- /**
5
- * Default fail-closed metadata resolver.
6
- *
7
- * Always returns `null`, meaning no peer metadata can be resolved.
8
- * This is intentional: without a real transport-specific resolver wired in,
9
- * no peers will pass the metadata resolution step and `findSellers()` will
10
- * return an empty list. Callers must supply a concrete `MetadataResolver`
11
- * implementation (e.g. one that fetches metadata over HTTP or a P2P channel)
12
- * to discover real peers.
13
- */
14
- export class DefaultMetadataResolver implements MetadataResolver {
15
- async resolve(_peer: PeerEndpoint): Promise<PeerMetadata | null> {
16
- return null;
17
- }
18
- }