kersiosocket 0.0.0-stage → 0.6.0-dev
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/BUILD-INFO.md +15 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +191 -2
- package/bench/wabinary.js +31 -0
- package/bridges/whatsmeow/README.md +42 -0
- package/docs/ENGINE-MATRIX.md +23 -0
- package/docs/PROTOCOL-ROADMAP.md +74 -0
- package/docs/RESEARCH-BASELINE.md +36 -0
- package/docs/WABINARY.md +37 -0
- package/examples/basic.js +10 -0
- package/examples/client-payload.js +21 -0
- package/examples/device-props.js +18 -0
- package/examples/sessions.js +11 -0
- package/examples/wabinary.js +15 -0
- package/licenses/THIRD-PARTY-NOTICES.md +12 -0
- package/package.json +31 -4
- package/scripts/import-token-table.js +95 -0
- package/src/backends/baileys.js +121 -0
- package/src/backends/base.js +102 -0
- package/src/backends/whatsmeow-process.js +202 -0
- package/src/backends/zapo.js +123 -0
- package/src/client/kersio-client.js +305 -0
- package/src/client/session-manager.js +67 -0
- package/src/index.js +36 -0
- package/src/pairing/adapter.js +115 -0
- package/src/pairing/lifecycle.js +120 -0
- package/src/pairing/qr.js +38 -0
- package/src/pairing/verified-commit.js +46 -0
- package/src/proto/client-payload.js +185 -0
- package/src/proto/device-props.js +143 -0
- package/src/proto/handshake.js +79 -0
- package/src/proto/wire.js +137 -0
- package/src/protocol/node-protocol.js +155 -0
- package/src/session/codec.js +25 -0
- package/src/session/encrypted-store.js +107 -0
- package/src/session/locking.js +233 -0
- package/src/session/manager.js +61 -0
- package/src/session/stores.js +154 -0
- package/src/socket/connection.js +278 -0
- package/src/socket/frame.js +77 -0
- package/src/socket/make.js +70 -0
- package/src/socket/noise.js +146 -0
- package/src/socket/transport.js +39 -0
- package/src/utils/crypto.js +52 -0
- package/src/utils/latency.js +32 -0
- package/src/utils/lru.js +76 -0
- package/src/wabinary/codec.js +374 -0
- package/src/wabinary/tags.js +25 -0
- package/src/wabinary/token-source.js +146 -0
- package/src/wabinary/tokens.json +5 -0
- package/test/backend-adapters.test.js +59 -0
- package/test/connection.test.js +173 -0
- package/test/device-props.test.js +67 -0
- package/test/encrypted-store.test.js +54 -0
- package/test/frame.test.js +37 -0
- package/test/helpers.js +43 -0
- package/test/locking-archive.test.js +49 -0
- package/test/node-protocol.test.js +90 -0
- package/test/noise.test.js +88 -0
- package/test/pairing-adapter.test.js +71 -0
- package/test/pairing.test.js +32 -0
- package/test/proto.test.js +64 -0
- package/test/session-manager.test.js +34 -0
- package/test/session.test.js +134 -0
- package/test/token-source.test.js +35 -0
- package/test/unified-client.test.js +63 -0
- package/test/verified-commit.test.js +43 -0
- package/test/wabinary.test.js +147 -0
- package/test/whatsmeow-process.test.js +43 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const LINKED_DEVICES_PREFIX = 'https://wa.me/settings/linked_devices#'
|
|
4
|
+
|
|
5
|
+
function bytes(value, name, length) {
|
|
6
|
+
if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) throw new TypeError(`${name} harus bytes`)
|
|
7
|
+
const out = Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
8
|
+
if (length !== undefined && out.length !== length) throw new RangeError(`${name} harus ${length} byte`)
|
|
9
|
+
return out
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build the observed linked-device QR payload. This only formats the QR payload;
|
|
14
|
+
* it does not implement registration, signature verification, or the live socket handler.
|
|
15
|
+
* The field layout is based on public whatsmeow/Baileys observations and must be
|
|
16
|
+
* checked against the WhatsApp Web version being targeted.
|
|
17
|
+
*/
|
|
18
|
+
function buildPairingQrPayload({ ref, noisePublicKey, identityPublicKey, advSecretKey, clientType }) {
|
|
19
|
+
const refBytes = typeof ref === 'string' ? Buffer.from(ref, 'utf8') : bytes(ref, 'ref')
|
|
20
|
+
if (refBytes.length === 0) throw new TypeError('ref pairing kosong')
|
|
21
|
+
const noise = bytes(noisePublicKey, 'noisePublicKey', 32)
|
|
22
|
+
const identity = bytes(identityPublicKey, 'identityPublicKey', 32)
|
|
23
|
+
const adv = bytes(advSecretKey, 'advSecretKey', 32)
|
|
24
|
+
if (typeof clientType !== 'string' || !/^[0-9a-z]$/.test(clientType)) throw new TypeError('clientType harus kode satu karakter yang telah diverifikasi')
|
|
25
|
+
return `${LINKED_DEVICES_PREFIX}${refBytes.toString('utf8')},${noise.toString('base64')},${identity.toString('base64')},${adv.toString('base64')},${clientType}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Extract only ref payloads from a decoded iq/pair-device node; no side effects. */
|
|
29
|
+
function extractPairingRefs(node) {
|
|
30
|
+
if (!node || node.tag !== 'iq' || !Array.isArray(node.content)) return []
|
|
31
|
+
const pairDevice = node.content.find((child) => child && child.tag === 'pair-device')
|
|
32
|
+
if (!pairDevice || !Array.isArray(pairDevice.content)) return []
|
|
33
|
+
return pairDevice.content
|
|
34
|
+
.filter((child) => child && child.tag === 'ref' && (Buffer.isBuffer(child.content) || child.content instanceof Uint8Array))
|
|
35
|
+
.map((child) => Buffer.from(child.content))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { LINKED_DEVICES_PREFIX, buildPairingQrPayload, extractPairingRefs }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fail-closed commit guard for a protocol-specific pair-success verifier.
|
|
5
|
+
* This function is intentionally not itself the WhatsApp ADV/signature verifier: callers
|
|
6
|
+
* must supply an independently validated implementation and test vectors.
|
|
7
|
+
*/
|
|
8
|
+
async function commitVerifiedPairSuccess({
|
|
9
|
+
node,
|
|
10
|
+
verifyPairSuccess,
|
|
11
|
+
store,
|
|
12
|
+
sessionId,
|
|
13
|
+
sendNode,
|
|
14
|
+
lifecycle
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (typeof verifyPairSuccess !== 'function') throw new TypeError('verifyPairSuccess wajib diisi; pairing tidak boleh dipercaya tanpa verifier')
|
|
17
|
+
if (!store || typeof store.set !== 'function') throw new TypeError('store wajib menyediakan set() untuk menyimpan credentials')
|
|
18
|
+
if (typeof sendNode !== 'function') throw new TypeError('sendNode wajib diisi untuk mengirim balasan pairing')
|
|
19
|
+
if (!lifecycle || typeof lifecycle.complete !== 'function') throw new TypeError('lifecycle pairing tidak valid')
|
|
20
|
+
|
|
21
|
+
const effectiveSessionId = sessionId || lifecycle.sessionId || 'default'
|
|
22
|
+
const result = await verifyPairSuccess(node)
|
|
23
|
+
if (!result || result.verified !== true || !result.checks ||
|
|
24
|
+
result.checks.deviceIdentity !== true ||
|
|
25
|
+
result.checks.accountSignature !== true ||
|
|
26
|
+
result.checks.advHmac !== true) {
|
|
27
|
+
const error = new Error('pair-success ditolak: pemeriksaan identity/signature/ADV-HMAC tidak lengkap atau gagal')
|
|
28
|
+
error.code = 'ERR_PAIR_SUCCESS_UNVERIFIED'
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
if (!result.credentials || typeof result.credentials !== 'object' || Array.isArray(result.credentials)) {
|
|
32
|
+
throw new TypeError('verifier tidak mengembalikan credentials terverifikasi')
|
|
33
|
+
}
|
|
34
|
+
if (!result.replyNode || typeof result.replyNode !== 'object' || typeof result.replyNode.tag !== 'string') {
|
|
35
|
+
throw new TypeError('verifier tidak mengembalikan node balasan pair-device-sign yang valid')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Persist before replying/finalizing. A crash after persistence is recoverable from
|
|
39
|
+
// auth state; a failed verification can never be saved or mark lifecycle as paired.
|
|
40
|
+
await store.set(`auth:${effectiveSessionId}:credentials`, result.credentials)
|
|
41
|
+
await sendNode(result.replyNode)
|
|
42
|
+
lifecycle.complete({ verified: true, sessionId: effectiveSessionId, completedAt: Date.now() })
|
|
43
|
+
return { verified: true, sessionId: effectiveSessionId, credentials: result.credentials }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { commitVerifiedPairSuccess }
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* KersioSocket's hand-maintained ClientPayload encoder.
|
|
4
|
+
* Field numbers are based on publicly documented/reverse-engineered protobuf
|
|
5
|
+
* schema observations and must be checked against the target WhatsApp Web build.
|
|
6
|
+
* This module does not perform pairing cryptography or manufacture key signatures.
|
|
7
|
+
*/
|
|
8
|
+
const { fieldVarint, fieldBool, fieldBytes, fieldString, fieldMessage, fieldFixed32, pack } = require('./wire')
|
|
9
|
+
|
|
10
|
+
const PLATFORM = Object.freeze({
|
|
11
|
+
ANDROID: 0, IOS: 1, WINDOWS_PHONE: 2, BLACKBERRY: 3, BLACKBERRYX: 4,
|
|
12
|
+
S40: 5, S60: 6, PYTHON_CLIENT: 7, TIZEN: 8, ENTERPRISE: 9,
|
|
13
|
+
SMB_ANDROID: 10, KAIOS: 11, SMB_IOS: 12, WINDOWS: 13, WEB: 14,
|
|
14
|
+
PORTAL: 15, GREEN_ANDROID: 16, GREEN_IPHONE: 17, BLUE_ANDROID: 18,
|
|
15
|
+
BLUE_IPHONE: 19, FBLITE_ANDROID: 20, MLITE_ANDROID: 21, IGLITE_ANDROID: 22,
|
|
16
|
+
PAGE: 23, MACOS: 24, OCULUS_MSG: 25, OCULUS_CALL: 26, MILAN: 27, CAPI: 28,
|
|
17
|
+
WEAROS: 29, ARDEVICE: 30, VRDEVICE: 31, BLUE_WEB: 32, IPAD: 33, TEST: 34,
|
|
18
|
+
SMART_GLASSES: 35, BLUE_VR: 36, AR_WRIST: 37
|
|
19
|
+
})
|
|
20
|
+
const RELEASE_CHANNEL = Object.freeze({ RELEASE: 0, BETA: 1, ALPHA: 2, DEBUG: 3 })
|
|
21
|
+
const WEB_SUB_PLATFORM = Object.freeze({ WEB_BROWSER: 0, APP_STORE: 1, WIN_STORE: 2, DARWIN: 3, WIN32: 4, WIN_HYBRID: 5 })
|
|
22
|
+
const CONNECT_REASON = Object.freeze({ PUSH: 0, USER_ACTIVATED: 1, SCHEDULED: 2, ERROR_RECONNECT: 3, NETWORK_SWITCH: 4, PING_RECONNECT: 5 })
|
|
23
|
+
const CONNECT_TYPE = Object.freeze({ CELLULAR_UNKNOWN: 0, WIFI_UNKNOWN: 1, CELLULAR_EDGE: 100, CELLULAR_IDEN: 101, CELLULAR_UMTS: 102, CELLULAR_EVDO: 103, CELLULAR_GPRS: 104, CELLULAR_HSDPA: 105, CELLULAR_HSUPA: 106, CELLULAR_HSPA: 107, CELLULAR_CDMA: 108, CELLULAR_1XRTT: 109, CELLULAR_EHRPD: 110, CELLULAR_LTE: 111, CELLULAR_HSPAP: 112 })
|
|
24
|
+
const PRODUCT = Object.freeze({ WHATSAPP: 0, MESSENGER: 1, INTEROP: 2, INTEROP_MSGR: 3, WHATSAPP_LID: 4 })
|
|
25
|
+
const DEVICE_TYPE = Object.freeze({ PHONE: 0, TABLET: 1, DESKTOP: 2, WEARABLE: 3, VR: 4 })
|
|
26
|
+
const IOS_APP_EXTENSION = Object.freeze({ SHARE_EXTENSION: 0, SERVICE_EXTENSION: 1, INTENTS_EXTENSION: 2 })
|
|
27
|
+
const TRAFFIC_ANONYMIZATION = Object.freeze({ OFF: 0, STANDARD: 1 })
|
|
28
|
+
const ACCOUNT_TYPE = Object.freeze({ DEFAULT: 0, GUEST: 1 })
|
|
29
|
+
|
|
30
|
+
function enumValue(table, value, name) {
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
if (!Object.hasOwn(table, value)) throw new TypeError(`${name} enum tidak dikenal: ${value}`)
|
|
33
|
+
return table[value]
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isInteger(value) || !Object.values(table).includes(value)) throw new TypeError(`${name} harus nama enum atau nilai enum yang dikenal`)
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nonEmptyString(value, name) {
|
|
40
|
+
if (typeof value !== 'string' || value.length === 0) throw new TypeError(`${name} wajib berupa string non-kosong`)
|
|
41
|
+
return value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeVersion(version, name) {
|
|
45
|
+
if (Array.isArray(version)) {
|
|
46
|
+
if (version.length < 3 || version.length > 5) throw new TypeError(`${name} harus memiliki 3 sampai 5 angka versi`)
|
|
47
|
+
return version.map((part, i) => {
|
|
48
|
+
if (!Number.isInteger(part) || part < 0 || part > 0xffffffff) throw new TypeError(`${name}[${i}] harus uint32`)
|
|
49
|
+
return part
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
if (version && typeof version === 'object') {
|
|
53
|
+
const ordered = ['primary', 'secondary', 'tertiary', 'quaternary', 'quinary']
|
|
54
|
+
const out = ordered.filter((key) => version[key] !== undefined).map((key) => version[key])
|
|
55
|
+
if (out.length < 3 || out.some((part) => !Number.isInteger(part) || part < 0 || part > 0xffffffff)) {
|
|
56
|
+
throw new TypeError(`${name} perlu primary/secondary/tertiary berupa uint32`)
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
throw new TypeError(`${name} harus array angka atau objek versi`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function encodeAppVersion(version) {
|
|
64
|
+
const parts = normalizeVersion(version, 'userAgent.appVersion')
|
|
65
|
+
return pack(parts.map((part, i) => fieldVarint(i + 1, part)))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function encodeUserAgent(input) {
|
|
69
|
+
if (!input || typeof input !== 'object') throw new TypeError('userAgent wajib diisi')
|
|
70
|
+
if (input.platform === undefined) throw new TypeError('userAgent.platform wajib diisi')
|
|
71
|
+
if (input.appVersion === undefined) throw new TypeError('userAgent.appVersion wajib diisi')
|
|
72
|
+
const parts = [
|
|
73
|
+
fieldVarint(1, enumValue(PLATFORM, input.platform, 'platform')),
|
|
74
|
+
fieldMessage(2, encodeAppVersion(input.appVersion)),
|
|
75
|
+
fieldString(3, input.mcc), fieldString(4, input.mnc),
|
|
76
|
+
fieldString(5, input.osVersion), fieldString(6, input.manufacturer),
|
|
77
|
+
fieldString(7, input.device), fieldString(8, input.osBuildNumber),
|
|
78
|
+
fieldString(9, input.phoneId),
|
|
79
|
+
input.releaseChannel === undefined ? null : fieldVarint(10, enumValue(RELEASE_CHANNEL, input.releaseChannel, 'releaseChannel')),
|
|
80
|
+
fieldString(11, input.localeLanguageIso6391 || input.localeLanguage),
|
|
81
|
+
fieldString(12, input.localeCountryIso31661Alpha2 || input.localeCountry),
|
|
82
|
+
fieldString(13, input.deviceBoard), fieldString(14, input.deviceExpId),
|
|
83
|
+
input.deviceType === undefined ? null : fieldVarint(15, enumValue(DEVICE_TYPE, input.deviceType, 'deviceType')),
|
|
84
|
+
fieldString(16, input.deviceModelType)
|
|
85
|
+
]
|
|
86
|
+
return pack(parts)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function encodeWebInfo(input) {
|
|
90
|
+
if (!input || typeof input !== 'object') throw new TypeError('webInfo wajib diisi')
|
|
91
|
+
const version = nonEmptyString(input.version, 'webInfo.version')
|
|
92
|
+
const parts = [fieldString(1, input.refToken), fieldString(2, version)]
|
|
93
|
+
if (input.webSubPlatform !== undefined) {
|
|
94
|
+
parts.push(fieldVarint(4, enumValue(WEB_SUB_PLATFORM, input.webSubPlatform, 'webSubPlatform')))
|
|
95
|
+
}
|
|
96
|
+
if (input.webdPayload !== undefined) {
|
|
97
|
+
if (!(Buffer.isBuffer(input.webdPayload) || input.webdPayload instanceof Uint8Array)) {
|
|
98
|
+
throw new TypeError('webInfo.webdPayload harus bytes protobuf yang sudah di-encode')
|
|
99
|
+
}
|
|
100
|
+
parts.push(fieldMessage(3, input.webdPayload))
|
|
101
|
+
}
|
|
102
|
+
return pack(parts)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function requireBytes(input, key, length, label) {
|
|
106
|
+
const value = input[key]
|
|
107
|
+
if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) throw new TypeError(`devicePairingData.${key} wajib berupa bytes`)
|
|
108
|
+
const buf = Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
109
|
+
if (length !== undefined && buf.length !== length) throw new RangeError(`devicePairingData.${key} harus ${length} byte`)
|
|
110
|
+
if (label === 'regid' && ![2, 3, 4].includes(buf.length)) throw new RangeError('devicePairingData.eRegid harus 2, 3, atau 4 byte')
|
|
111
|
+
if (label === 'deviceProps' && buf.length === 0) throw new RangeError('devicePairingData.deviceProps tidak boleh kosong')
|
|
112
|
+
return buf
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encodeDevicePairingData(input) {
|
|
116
|
+
if (!input || typeof input !== 'object') throw new TypeError('devicePairingData wajib berupa objek')
|
|
117
|
+
return pack([
|
|
118
|
+
fieldBytes(1, requireBytes(input, 'eRegid', 4)),
|
|
119
|
+
fieldBytes(2, requireBytes(input, 'eKeytype', 1)),
|
|
120
|
+
fieldBytes(3, requireBytes(input, 'eIdent', 32)),
|
|
121
|
+
fieldBytes(4, requireBytes(input, 'eSkeyId', 3)),
|
|
122
|
+
fieldBytes(5, requireBytes(input, 'eSkeyVal', 32)),
|
|
123
|
+
fieldBytes(6, requireBytes(input, 'eSkeySig', 64)),
|
|
124
|
+
fieldBytes(7, requireBytes(input, 'buildHash', 16)),
|
|
125
|
+
fieldBytes(8, requireBytes(input, 'deviceProps', undefined, 'deviceProps'))
|
|
126
|
+
])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Encode an explicit ClientPayload object to bytes for Noise ClientFinish.
|
|
131
|
+
* `mode: 'registration'` requires a complete, externally prepared pairing bundle;
|
|
132
|
+
* `mode: 'login'` requires an existing WhatsApp numeric username.
|
|
133
|
+
*
|
|
134
|
+
* Version values are intentionally supplied by the caller, not hard-coded, because
|
|
135
|
+
* WhatsApp Web changes them. A successful encode does not prove server compatibility.
|
|
136
|
+
*/
|
|
137
|
+
function encodeClientPayload(input) {
|
|
138
|
+
if (!input || typeof input !== 'object' || Buffer.isBuffer(input)) throw new TypeError('clientPayload harus berupa objek terstruktur')
|
|
139
|
+
const mode = input.mode || (input.devicePairingData ? 'registration' : 'login')
|
|
140
|
+
if (mode !== 'registration' && mode !== 'login') throw new TypeError("mode harus 'registration' atau 'login'")
|
|
141
|
+
if (mode === 'registration' && input.username !== undefined) throw new TypeError('registration tidak boleh memakai username login')
|
|
142
|
+
if (input.agent !== undefined) throw new TypeError('ClientPayload.agent tidak tersedia di schema yang dipatok; field 17 tidak didefinisikan di referensi ini')
|
|
143
|
+
if (mode === 'registration' && input.devicePairingData === undefined) throw new TypeError('registration memerlukan devicePairingData lengkap')
|
|
144
|
+
if (mode === 'login' && input.devicePairingData !== undefined) throw new TypeError('login tidak boleh membawa devicePairingData registrasi')
|
|
145
|
+
if (mode === 'login') {
|
|
146
|
+
const username = input.username
|
|
147
|
+
if (typeof username !== 'string' || !/^\d{5,20}$/.test(username)) throw new TypeError('username login harus string digit nomor akun, tanpa tanda +')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const userAgent = encodeUserAgent(input.userAgent)
|
|
151
|
+
const webInfo = encodeWebInfo(input.webInfo)
|
|
152
|
+
const payload = [
|
|
153
|
+
input.username === undefined ? null : fieldVarint(1, BigInt(input.username)),
|
|
154
|
+
fieldBool(3, input.passive),
|
|
155
|
+
fieldMessage(5, userAgent), fieldMessage(6, webInfo),
|
|
156
|
+
fieldString(7, input.pushName),
|
|
157
|
+
fieldFixed32(9, input.sessionId),
|
|
158
|
+
fieldBool(10, input.shortConnect),
|
|
159
|
+
input.connectType === undefined ? null : fieldVarint(12, enumValue(CONNECT_TYPE, input.connectType, 'connectType')),
|
|
160
|
+
input.connectReason === undefined ? null : fieldVarint(13, enumValue(CONNECT_REASON, input.connectReason, 'connectReason')),
|
|
161
|
+
fieldVarint(16, input.connectAttemptCount), fieldVarint(18, input.device),
|
|
162
|
+
input.devicePairingData === undefined ? null : fieldMessage(19, encodeDevicePairingData(input.devicePairingData)),
|
|
163
|
+
input.product === undefined ? null : fieldVarint(20, enumValue(PRODUCT, input.product, 'product')),
|
|
164
|
+
fieldBytes(21, input.fbCat), fieldBytes(22, input.fbUserAgent),
|
|
165
|
+
fieldBool(23, input.oc), fieldVarint(24, input.lc),
|
|
166
|
+
input.iosAppExtension === undefined ? null : fieldVarint(30, enumValue(IOS_APP_EXTENSION, input.iosAppExtension, 'iosAppExtension')),
|
|
167
|
+
input.fbAppId === undefined ? null : fieldVarint(31, input.fbAppId), fieldBytes(32, input.fbDeviceId),
|
|
168
|
+
fieldBool(33, input.pull), fieldBytes(34, input.paddingBytes),
|
|
169
|
+
fieldVarint(36, input.yearClass), fieldVarint(37, input.memClass),
|
|
170
|
+
input.trafficAnonymization === undefined ? null : fieldVarint(40, enumValue(TRAFFIC_ANONYMIZATION, input.trafficAnonymization, 'trafficAnonymization')),
|
|
171
|
+
fieldBool(41, input.lidDbMigrated),
|
|
172
|
+
input.accountType === undefined ? null : fieldVarint(42, enumValue(ACCOUNT_TYPE, input.accountType, 'accountType')),
|
|
173
|
+
fieldFixed32(43, input.connectionSequenceInfo), fieldBool(44, input.paaLink),
|
|
174
|
+
fieldVarint(45, input.preacksCount), fieldVarint(46, input.processingQueueSize)
|
|
175
|
+
]
|
|
176
|
+
const encoded = pack(payload)
|
|
177
|
+
if (encoded.length === 0) throw new Error('ClientPayload hasil encode kosong')
|
|
178
|
+
return encoded
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
encodeClientPayload, encodeUserAgent, encodeWebInfo, encodeDevicePairingData,
|
|
183
|
+
PLATFORM, RELEASE_CHANNEL, WEB_SUB_PLATFORM, CONNECT_REASON, CONNECT_TYPE, PRODUCT, DEVICE_TYPE,
|
|
184
|
+
IOS_APP_EXTENSION, TRAFFIC_ANONYMIZATION, ACCOUNT_TYPE
|
|
185
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Explicit encoder for the pinned WAProto DeviceProps schema snapshot.
|
|
4
|
+
* This builds protobuf bytes only; it does not create/verify Signal keys or signatures.
|
|
5
|
+
*/
|
|
6
|
+
const crypto = require('node:crypto')
|
|
7
|
+
const { fieldVarint, fieldBool, fieldString, fieldMessage, fieldBytes, pack } = require('./wire')
|
|
8
|
+
|
|
9
|
+
const PLATFORM_TYPE = Object.freeze({
|
|
10
|
+
UNKNOWN: 0, CHROME: 1, FIREFOX: 2, IE: 3, OPERA: 4, SAFARI: 5, EDGE: 6,
|
|
11
|
+
DESKTOP: 7, IPAD: 8, ANDROID_TABLET: 9, OHANA: 10, ALOHA: 11, CATALINA: 12,
|
|
12
|
+
TCL_TV: 13, IOS_PHONE: 14, IOS_CATALYST: 15, ANDROID_PHONE: 16,
|
|
13
|
+
ANDROID_AMBIGUOUS: 17, WEAR_OS: 18, AR_WRIST: 19, AR_DEVICE: 20, UWP: 21,
|
|
14
|
+
VR: 22, CLOUD_API: 23, SMARTGLASSES: 24
|
|
15
|
+
})
|
|
16
|
+
const HISTORY_FIELDS = Object.freeze([
|
|
17
|
+
['fullSyncDaysLimit', 1, 'uint'], ['fullSyncSizeMbLimit', 2, 'uint'], ['storageQuotaMb', 3, 'uint'],
|
|
18
|
+
['inlineInitialPayloadInE2EeMsg', 4, 'bool'], ['recentSyncDaysLimit', 5, 'uint'],
|
|
19
|
+
['supportCallLogHistory', 6, 'bool'], ['supportBotUserAgentChatHistory', 7, 'bool'],
|
|
20
|
+
['supportCagReactionsAndPolls', 8, 'bool'], ['supportBizHostedMsg', 9, 'bool'],
|
|
21
|
+
['supportRecentSyncChunkMessageCountTuning', 10, 'bool'], ['supportHostedGroupMsg', 11, 'bool'],
|
|
22
|
+
['supportFbidBotChatHistory', 12, 'bool'], ['supportAddOnHistorySyncMigration', 13, 'bool'],
|
|
23
|
+
['supportMessageAssociation', 14, 'bool'], ['supportGroupHistory', 15, 'bool'],
|
|
24
|
+
['onDemandReady', 16, 'bool'], ['supportGuestChat', 17, 'bool'],
|
|
25
|
+
['completeOnDemandReady', 18, 'bool'], ['thumbnailSyncDaysLimit', 19, 'uint']
|
|
26
|
+
])
|
|
27
|
+
const VERSION_FIELDS = ['primary', 'secondary', 'tertiary', 'quaternary', 'quinary']
|
|
28
|
+
|
|
29
|
+
function versionParts(version, label = 'version') {
|
|
30
|
+
let parts
|
|
31
|
+
if (Array.isArray(version)) parts = version
|
|
32
|
+
else if (version && typeof version === 'object') parts = VERSION_FIELDS.filter((k) => version[k] !== undefined).map((k) => version[k])
|
|
33
|
+
else throw new TypeError(`${label} harus array atau objek versi`)
|
|
34
|
+
if (parts.length < 3 || parts.length > 5 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 0xffffffff)) {
|
|
35
|
+
throw new TypeError(`${label} harus berisi 3–5 uint32`)
|
|
36
|
+
}
|
|
37
|
+
return parts
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function encodeVersion(version, label) {
|
|
41
|
+
return pack(versionParts(version, label).map((n, i) => fieldVarint(i + 1, n)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function enumField(table, value, field, label) {
|
|
45
|
+
if (value === undefined || value === null) return null
|
|
46
|
+
let n = value
|
|
47
|
+
if (typeof n === 'string') {
|
|
48
|
+
if (!Object.hasOwn(table, n)) throw new TypeError(`${label} enum tidak dikenal: ${n}`)
|
|
49
|
+
n = table[n]
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isInteger(n) || !Object.values(table).includes(n)) throw new TypeError(`${label} harus nilai enum yang dikenal`)
|
|
52
|
+
return fieldVarint(field, n)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function encodeHistorySyncConfig(input) {
|
|
56
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('historySyncConfig harus objek')
|
|
57
|
+
const known = new Set(HISTORY_FIELDS.map(([key]) => key))
|
|
58
|
+
for (const key of Object.keys(input)) if (!known.has(key)) throw new TypeError(`historySyncConfig field tidak dikenal: ${key}`)
|
|
59
|
+
const fields = []
|
|
60
|
+
for (const [key, no, type] of HISTORY_FIELDS) {
|
|
61
|
+
if (input[key] === undefined || input[key] === null) continue
|
|
62
|
+
if (type === 'bool') fields.push(fieldBool(no, input[key]))
|
|
63
|
+
else {
|
|
64
|
+
if (!Number.isInteger(input[key]) || input[key] < 0 || input[key] > 0xffffffff) throw new TypeError(`historySyncConfig.${key} harus uint32`)
|
|
65
|
+
fields.push(fieldVarint(no, input[key]))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return pack(fields)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function encodeDeviceProps(input) {
|
|
72
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('deviceProps harus objek')
|
|
73
|
+
const allowed = new Set(['os', 'version', 'platformType', 'requireFullSync', 'historySyncConfig'])
|
|
74
|
+
for (const key of Object.keys(input)) if (!allowed.has(key)) throw new TypeError(`DeviceProps field tidak dikenal: ${key}`)
|
|
75
|
+
const fields = [
|
|
76
|
+
input.os === undefined ? null : fieldString(1, input.os),
|
|
77
|
+
input.version === undefined ? null : fieldMessage(2, encodeVersion(input.version, 'DeviceProps.version')),
|
|
78
|
+
enumField(PLATFORM_TYPE, input.platformType, 3, 'DeviceProps.platformType'),
|
|
79
|
+
fieldBool(4, input.requireFullSync),
|
|
80
|
+
input.historySyncConfig === undefined ? null : fieldMessage(5, encodeHistorySyncConfig(input.historySyncConfig))
|
|
81
|
+
]
|
|
82
|
+
const encoded = pack(fields)
|
|
83
|
+
if (encoded.length === 0) throw new TypeError('DeviceProps tidak boleh kosong')
|
|
84
|
+
return encoded
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Encode a registration ID / signed-prekey ID in unsigned big-endian form. */
|
|
88
|
+
function encodeBigEndianUnsigned(value, width) {
|
|
89
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError('nilai big-endian harus integer non-negatif yang aman')
|
|
90
|
+
if (!Number.isInteger(width) || width < 1 || width > 6) throw new RangeError('width harus 1..6 byte')
|
|
91
|
+
if (BigInt(value) >= (1n << BigInt(width * 8))) throw new RangeError(`nilai tidak muat dalam ${width} byte`)
|
|
92
|
+
const out = Buffer.alloc(width)
|
|
93
|
+
let n = BigInt(value)
|
|
94
|
+
for (let i = width - 1; i >= 0; i--) { out[i] = Number(n & 0xffn); n >>= 8n }
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Matches the observed registration buildHash: MD5 of dotted client version. */
|
|
99
|
+
function buildAppVersionHash(version) {
|
|
100
|
+
const parts = versionParts(version, 'appVersion')
|
|
101
|
+
return crypto.createHash('md5').update(parts.join('.'), 'utf8').digest()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the DevicePairingRegistrationData fields from caller-created key material.
|
|
106
|
+
* No keys/signatures are generated here; signedPreKey.signature must already be valid.
|
|
107
|
+
*/
|
|
108
|
+
function createDevicePairingData({ registrationId, identityPublicKey, signedPreKey, appVersion, deviceProps, eKeytype = Buffer.from([5]) }) {
|
|
109
|
+
if (!signedPreKey || typeof signedPreKey !== 'object') throw new TypeError('signedPreKey harus berisi keyId, publicKey, signature')
|
|
110
|
+
if (!Buffer.isBuffer(identityPublicKey) && !(identityPublicKey instanceof Uint8Array)) throw new TypeError('identityPublicKey harus bytes')
|
|
111
|
+
if (!Buffer.isBuffer(signedPreKey.publicKey) && !(signedPreKey.publicKey instanceof Uint8Array)) throw new TypeError('signedPreKey.publicKey harus bytes')
|
|
112
|
+
if (!Buffer.isBuffer(signedPreKey.signature) && !(signedPreKey.signature instanceof Uint8Array)) throw new TypeError('signedPreKey.signature harus bytes')
|
|
113
|
+
if (!Buffer.isBuffer(eKeytype) && !(eKeytype instanceof Uint8Array)) throw new TypeError('eKeytype harus bytes')
|
|
114
|
+
const identity = Buffer.from(identityPublicKey)
|
|
115
|
+
const preKey = Buffer.from(signedPreKey.publicKey)
|
|
116
|
+
const signature = Buffer.from(signedPreKey.signature)
|
|
117
|
+
const keyType = Buffer.from(eKeytype)
|
|
118
|
+
if (identity.length !== 32) throw new RangeError('identityPublicKey harus 32 byte')
|
|
119
|
+
if (preKey.length !== 32) throw new RangeError('signedPreKey.publicKey harus 32 byte')
|
|
120
|
+
if (signature.length !== 64) throw new RangeError('signedPreKey.signature harus 64 byte')
|
|
121
|
+
if (keyType.length !== 1) throw new RangeError('eKeytype harus 1 byte pada schema ini')
|
|
122
|
+
const props = Buffer.isBuffer(deviceProps) || deviceProps instanceof Uint8Array ? Buffer.from(deviceProps) : encodeDeviceProps(deviceProps)
|
|
123
|
+
if (!props.length) throw new RangeError('deviceProps tidak boleh kosong')
|
|
124
|
+
return {
|
|
125
|
+
eRegid: encodeBigEndianUnsigned(registrationId, 4),
|
|
126
|
+
eKeytype: keyType,
|
|
127
|
+
eIdent: identity,
|
|
128
|
+
eSkeyId: encodeBigEndianUnsigned(signedPreKey.keyId, 3),
|
|
129
|
+
eSkeyVal: preKey,
|
|
130
|
+
eSkeySig: signature,
|
|
131
|
+
buildHash: buildAppVersionHash(appVersion),
|
|
132
|
+
deviceProps: props
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
PLATFORM_TYPE,
|
|
138
|
+
encodeDeviceProps,
|
|
139
|
+
encodeHistorySyncConfig,
|
|
140
|
+
encodeBigEndianUnsigned,
|
|
141
|
+
buildAppVersionHash,
|
|
142
|
+
createDevicePairingData
|
|
143
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Encoder/decoder protobuf MINIMAL khusus pesan handshake.
|
|
4
|
+
* Hanya mendukung field length-delimited (wire type 2) + skip varint.
|
|
5
|
+
* Nanti (v0.2) diganti proto lengkap; ini sengaja kecil supaya cepat dimuat.
|
|
6
|
+
*
|
|
7
|
+
* HandshakeMessage { clientHello = 2; serverHello = 3; clientFinish = 4 }
|
|
8
|
+
* ClientHello { ephemeral = 1; static = 2; payload = 3 }
|
|
9
|
+
* ServerHello { ephemeral = 1; static = 2; payload = 3 }
|
|
10
|
+
* ClientFinish { static = 1; payload = 2 }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function varint(n) {
|
|
14
|
+
const out = []
|
|
15
|
+
while (n > 0x7f) { out.push((n & 0x7f) | 0x80); n >>>= 7 }
|
|
16
|
+
out.push(n)
|
|
17
|
+
return Buffer.from(out)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function field(num, buf) {
|
|
21
|
+
return Buffer.concat([varint((num << 3) | 2), varint(buf.length), buf])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decodeFields(buf) {
|
|
25
|
+
const fields = new Map()
|
|
26
|
+
let i = 0
|
|
27
|
+
const readVarint = () => {
|
|
28
|
+
let result = 0, shift = 0, b
|
|
29
|
+
do {
|
|
30
|
+
if (i >= buf.length) throw new Error('protobuf: varint terpotong')
|
|
31
|
+
b = buf[i++]
|
|
32
|
+
result |= (b & 0x7f) << shift
|
|
33
|
+
shift += 7
|
|
34
|
+
} while (b & 0x80)
|
|
35
|
+
return result >>> 0
|
|
36
|
+
}
|
|
37
|
+
while (i < buf.length) {
|
|
38
|
+
const tag = readVarint()
|
|
39
|
+
const num = tag >>> 3, wire = tag & 7
|
|
40
|
+
if (wire === 0) readVarint()
|
|
41
|
+
else if (wire === 2) {
|
|
42
|
+
const len = readVarint()
|
|
43
|
+
if (i + len > buf.length) throw new Error('protobuf: field melebihi buffer')
|
|
44
|
+
fields.set(num, buf.subarray(i, i + len))
|
|
45
|
+
i += len
|
|
46
|
+
} else throw new Error(`protobuf: wire type ${wire} belum didukung`)
|
|
47
|
+
}
|
|
48
|
+
return fields
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const encodeClientHello = (ephemeral) => field(2, field(1, ephemeral))
|
|
52
|
+
const encodeServerHello = ({ ephemeral, static: s, payload }) =>
|
|
53
|
+
field(3, Buffer.concat([field(1, ephemeral), field(2, s), field(3, payload)]))
|
|
54
|
+
const encodeClientFinish = ({ static: s, payload }) =>
|
|
55
|
+
field(4, Buffer.concat([field(1, s), field(2, payload)]))
|
|
56
|
+
|
|
57
|
+
function need(map, num, name) {
|
|
58
|
+
const v = map.get(num)
|
|
59
|
+
if (!v) throw new Error(`handshake: field ${name} tidak ada`)
|
|
60
|
+
return v
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function decodeClientHello(buf) {
|
|
64
|
+
const inner = decodeFields(need(decodeFields(buf), 2, 'clientHello'))
|
|
65
|
+
return { ephemeral: need(inner, 1, 'ephemeral') }
|
|
66
|
+
}
|
|
67
|
+
function decodeServerHello(buf) {
|
|
68
|
+
const inner = decodeFields(need(decodeFields(buf), 3, 'serverHello'))
|
|
69
|
+
return { ephemeral: need(inner, 1, 'ephemeral'), static: need(inner, 2, 'static'), payload: need(inner, 3, 'payload') }
|
|
70
|
+
}
|
|
71
|
+
function decodeClientFinish(buf) {
|
|
72
|
+
const inner = decodeFields(need(decodeFields(buf), 4, 'clientFinish'))
|
|
73
|
+
return { static: need(inner, 1, 'static'), payload: need(inner, 2, 'payload') }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
encodeClientHello, encodeServerHello, encodeClientFinish,
|
|
78
|
+
decodeClientHello, decodeServerHello, decodeClientFinish
|
|
79
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Tiny protobuf wire-format codec used by KersioSocket.
|
|
4
|
+
*
|
|
5
|
+
* This intentionally implements the protobuf wire primitives, not a generated
|
|
6
|
+
* copy of another project's WAProto code. Field meanings live in our own schema
|
|
7
|
+
* modules. Supported wire types: varint (0), fixed64 (1), bytes/message (2),
|
|
8
|
+
* and fixed32 (5). No implicit string conversion for binary fields.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_FIELD_NUMBER = 0x1fffffff
|
|
12
|
+
|
|
13
|
+
function asBigInt(value, name = 'value') {
|
|
14
|
+
if (typeof value === 'bigint') {
|
|
15
|
+
if (value < 0n || value > 0xffffffffffffffffn) throw new RangeError(`${name} di luar rentang uint64`)
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} harus integer aman non-negatif`)
|
|
19
|
+
return BigInt(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function encodeVarint(value) {
|
|
23
|
+
let n = asBigInt(value)
|
|
24
|
+
const out = []
|
|
25
|
+
while (n > 0x7fn) {
|
|
26
|
+
out.push(Number((n & 0x7fn) | 0x80n))
|
|
27
|
+
n >>= 7n
|
|
28
|
+
}
|
|
29
|
+
out.push(Number(n))
|
|
30
|
+
return Buffer.from(out)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tag(fieldNumber, wireType) {
|
|
34
|
+
if (!Number.isInteger(fieldNumber) || fieldNumber < 1 || fieldNumber > MAX_FIELD_NUMBER) {
|
|
35
|
+
throw new RangeError('nomor field protobuf tidak valid')
|
|
36
|
+
}
|
|
37
|
+
if (![0, 1, 2, 5].includes(wireType)) throw new RangeError('wire type protobuf tidak didukung')
|
|
38
|
+
return encodeVarint(BigInt(fieldNumber * 8 + wireType))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fieldVarint(fieldNumber, value) {
|
|
42
|
+
if (value === undefined || value === null) return null
|
|
43
|
+
return Buffer.concat([tag(fieldNumber, 0), encodeVarint(value)])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fieldBool(fieldNumber, value) {
|
|
47
|
+
if (value === undefined || value === null) return null
|
|
48
|
+
if (typeof value !== 'boolean') throw new TypeError(`field ${fieldNumber} harus boolean`)
|
|
49
|
+
return fieldVarint(fieldNumber, value ? 1 : 0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fieldBytes(fieldNumber, value) {
|
|
53
|
+
if (value === undefined || value === null) return null
|
|
54
|
+
if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
55
|
+
throw new TypeError(`field ${fieldNumber} harus Buffer/Uint8Array`)
|
|
56
|
+
}
|
|
57
|
+
const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
58
|
+
return Buffer.concat([tag(fieldNumber, 2), encodeVarint(bytes.length), bytes])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fieldString(fieldNumber, value) {
|
|
62
|
+
if (value === undefined || value === null) return null
|
|
63
|
+
if (typeof value !== 'string') throw new TypeError(`field ${fieldNumber} harus string`)
|
|
64
|
+
return fieldBytes(fieldNumber, Buffer.from(value, 'utf8'))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fieldMessage(fieldNumber, value) {
|
|
68
|
+
return fieldBytes(fieldNumber, value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fieldFixed32(fieldNumber, value) {
|
|
72
|
+
if (value === undefined || value === null) return null
|
|
73
|
+
if (!Number.isInteger(value) || value < -0x80000000 || value > 0x7fffffff) {
|
|
74
|
+
throw new RangeError(`field ${fieldNumber} harus signed int32`)
|
|
75
|
+
}
|
|
76
|
+
const data = Buffer.allocUnsafe(4)
|
|
77
|
+
data.writeInt32LE(value)
|
|
78
|
+
return Buffer.concat([tag(fieldNumber, 5), data])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pack(parts) {
|
|
82
|
+
const present = parts.filter((part) => part !== null && part !== undefined)
|
|
83
|
+
return present.length ? Buffer.concat(present) : Buffer.alloc(0)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readVarint(buffer, state) {
|
|
87
|
+
let value = 0n
|
|
88
|
+
let shift = 0n
|
|
89
|
+
for (let count = 0; count < 10; count++) {
|
|
90
|
+
if (state.offset >= buffer.length) throw new Error('protobuf: varint terpotong')
|
|
91
|
+
const byte = buffer[state.offset++]
|
|
92
|
+
value |= BigInt(byte & 0x7f) << shift
|
|
93
|
+
if ((byte & 0x80) === 0) return value
|
|
94
|
+
shift += 7n
|
|
95
|
+
}
|
|
96
|
+
throw new Error('protobuf: varint melebihi 10 byte')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Decode wire entries without applying any application-specific schema. */
|
|
100
|
+
function parseFields(input) {
|
|
101
|
+
const buffer = Buffer.from(input)
|
|
102
|
+
const state = { offset: 0 }
|
|
103
|
+
const fields = []
|
|
104
|
+
while (state.offset < buffer.length) {
|
|
105
|
+
const rawTag = readVarint(buffer, state)
|
|
106
|
+
const fieldNumber = Number(rawTag >> 3n)
|
|
107
|
+
const wireType = Number(rawTag & 7n)
|
|
108
|
+
if (fieldNumber < 1 || fieldNumber > MAX_FIELD_NUMBER) throw new Error('protobuf: nomor field tidak valid')
|
|
109
|
+
let value
|
|
110
|
+
if (wireType === 0) {
|
|
111
|
+
value = readVarint(buffer, state)
|
|
112
|
+
} else if (wireType === 1) {
|
|
113
|
+
if (state.offset + 8 > buffer.length) throw new Error('protobuf: fixed64 terpotong')
|
|
114
|
+
value = Buffer.from(buffer.subarray(state.offset, state.offset + 8))
|
|
115
|
+
state.offset += 8
|
|
116
|
+
} else if (wireType === 2) {
|
|
117
|
+
const length = readVarint(buffer, state)
|
|
118
|
+
if (length > BigInt(buffer.length - state.offset)) throw new Error('protobuf: panjang field melebihi buffer')
|
|
119
|
+
const end = state.offset + Number(length)
|
|
120
|
+
value = Buffer.from(buffer.subarray(state.offset, end))
|
|
121
|
+
state.offset = end
|
|
122
|
+
} else if (wireType === 5) {
|
|
123
|
+
if (state.offset + 4 > buffer.length) throw new Error('protobuf: fixed32 terpotong')
|
|
124
|
+
value = Buffer.from(buffer.subarray(state.offset, state.offset + 4))
|
|
125
|
+
state.offset += 4
|
|
126
|
+
} else {
|
|
127
|
+
throw new Error(`protobuf: wire type ${wireType} tidak didukung`)
|
|
128
|
+
}
|
|
129
|
+
fields.push({ fieldNumber, wireType, value })
|
|
130
|
+
}
|
|
131
|
+
return fields
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
encodeVarint, fieldVarint, fieldBool, fieldBytes, fieldString,
|
|
136
|
+
fieldMessage, fieldFixed32, pack, parseFields
|
|
137
|
+
}
|