ocremote 1.3.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 +277 -0
- package/install.sh +261 -0
- package/lib/crypto.mjs +146 -0
- package/lib/identity.mjs +165 -0
- package/lib/relay.mjs +495 -0
- package/lib/rpc.mjs +114 -0
- package/oc-remote.mjs +2390 -0
- package/package.json +34 -0
- package/vendor/qrcode.js +2297 -0
package/lib/rpc.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { b64 } from './crypto.mjs'
|
|
2
|
+
|
|
3
|
+
const MAX_CONCURRENT = 8
|
|
4
|
+
const MAX_UPLOAD = 24 << 20
|
|
5
|
+
|
|
6
|
+
export class RpcAdapter {
|
|
7
|
+
constructor({ baseUrl, authHeader, logger }) {
|
|
8
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '')
|
|
9
|
+
this.authHeader = authHeader
|
|
10
|
+
this.logger = logger
|
|
11
|
+
this.active = new Map()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
log(message) {
|
|
15
|
+
this.logger?.(message)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
sessionMap(session) {
|
|
19
|
+
if (!this.active.has(session.id)) this.active.set(session.id, new Map())
|
|
20
|
+
return this.active.get(session.id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
cancel(session, id) {
|
|
24
|
+
const controller = this.sessionMap(session).get(id)
|
|
25
|
+
if (controller) {
|
|
26
|
+
controller.abort()
|
|
27
|
+
this.log(`rpc ${id} cancelled`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cancelAll(session) {
|
|
32
|
+
const map = this.active.get(session.id)
|
|
33
|
+
if (!map) return
|
|
34
|
+
for (const controller of map.values()) {
|
|
35
|
+
try {
|
|
36
|
+
controller.abort()
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
this.active.delete(session.id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async handle(session, msg) {
|
|
43
|
+
const id = msg.id
|
|
44
|
+
if (typeof id !== 'number' || typeof msg.path !== 'string') return
|
|
45
|
+
const map = this.sessionMap(session)
|
|
46
|
+
if (map.size >= MAX_CONCURRENT) {
|
|
47
|
+
session.sendSealed({ t: 'res', id, error: 'too_many_requests' })
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (!msg.path.startsWith('/') || msg.path.includes('..')) {
|
|
51
|
+
session.sendSealed({ t: 'res', id, error: 'bad_path' })
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const controller = new AbortController()
|
|
56
|
+
map.set(id, controller)
|
|
57
|
+
const stream = msg.stream === true
|
|
58
|
+
let opened = false
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(`${this.baseUrl}${msg.path}`)
|
|
61
|
+
if (msg.query && typeof msg.query === 'object') {
|
|
62
|
+
for (const [key, value] of Object.entries(msg.query)) {
|
|
63
|
+
if (value === undefined || value === null) continue
|
|
64
|
+
url.searchParams.set(key, String(value))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const headers = { authorization: this.authHeader }
|
|
68
|
+
const init = { method: msg.method || 'GET', headers, signal: controller.signal }
|
|
69
|
+
if (msg.body) {
|
|
70
|
+
const body = Buffer.from(String(msg.body), 'base64')
|
|
71
|
+
if (body.length > MAX_UPLOAD) {
|
|
72
|
+
session.sendSealed({ t: 'res', id, error: 'body_too_large' })
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
init.body = body
|
|
76
|
+
headers['content-type'] = msg.contentType || 'application/json'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const res = await fetch(url, init)
|
|
80
|
+
if (!stream) {
|
|
81
|
+
const buffer = Buffer.from(await res.arrayBuffer())
|
|
82
|
+
session.sendSealed({
|
|
83
|
+
t: 'res',
|
|
84
|
+
id,
|
|
85
|
+
status: res.status,
|
|
86
|
+
contentType: res.headers.get('content-type') || '',
|
|
87
|
+
body: b64(buffer),
|
|
88
|
+
})
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
opened = true
|
|
93
|
+
session.sendSealed({ t: 'open', id, status: res.status, contentType: res.headers.get('content-type') || '' })
|
|
94
|
+
if (!res.body) {
|
|
95
|
+
session.sendSealed({ t: 'end', id })
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
for await (const chunk of res.body) {
|
|
99
|
+
if (controller.signal.aborted) break
|
|
100
|
+
session.sendSealed({ t: 'chunk', id, data: b64(chunk) })
|
|
101
|
+
}
|
|
102
|
+
session.sendSealed({ t: 'end', id })
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (controller.signal.aborted) {
|
|
105
|
+
session.sendSealed(opened ? { t: 'end', id, aborted: true } : { t: 'res', id, error: 'aborted' })
|
|
106
|
+
} else {
|
|
107
|
+
this.log(`rpc ${msg.method} ${msg.path} failed: ${err.message}`)
|
|
108
|
+
session.sendSealed(opened ? { t: 'end', id, error: 'stream_failed' } : { t: 'res', id, error: 'opencode_unreachable' })
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
map.delete(id)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|