Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,29 @@ jobs:
- name: run sqlite tests
run: npm test -w packages/store-sqlite

test-migrate:
runs-on: ubuntu-latest
needs: test-core

steps:
- name: checkout repository
uses: actions/checkout@v4

- name: setup node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: install dependencies
run: npm ci

- name: build
run: npm run build && npx turbo run build --filter=@zapo-js/store-migrate

- name: run migrate tests
run: npm test -w packages/store-migrate

test-mysql:
runs-on: ubuntu-latest
needs: test-core
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module.exports = [
'./packages/store-postgres/tsconfig.json',
'./packages/store-redis/tsconfig.json',
'./packages/store-mongo/tsconfig.json',
'./packages/store-migrate/tsconfig.json',
'./packages/media-utils/tsconfig.json',
'./packages/fake-server/tsconfig.json'
]
Expand Down Expand Up @@ -79,6 +80,7 @@ module.exports = [
'./packages/store-postgres/tsconfig.json',
'./packages/store-redis/tsconfig.json',
'./packages/store-mongo/tsconfig.json',
'./packages/store-migrate/tsconfig.json',
'./packages/media-utils/tsconfig.json',
'./packages/fake-server/tsconfig.json'
]
Expand Down
19 changes: 18 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions packages/store-migrate/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@zapo-js/store-migrate",
"version": "0.1.0",
"description": "Pure converters for migrating Baileys and whatsmeow stores into zapo-js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./baileys": {
"types": "./dist/baileys/index.d.ts",
"require": "./dist/baileys/index.js",
"default": "./dist/baileys/index.js"
},
"./whatsmeow": {
"types": "./dist/whatsmeow/index.d.ts",
"require": "./dist/whatsmeow/index.js",
"default": "./dist/whatsmeow/index.js"
}
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc -p tsconfig.build.cjs.json",
"typecheck": "tsc --noEmit",
"test": "node --import tsx --test \"src/**/__tests__/**/*.test.ts\""
},
"peerDependencies": {
"zapo-js": ">=0.1.0"
},
"devDependencies": {
"zapo-js": "file:../.."
},
"engines": {
"node": ">=20.9.0"
}
}
63 changes: 63 additions & 0 deletions packages/store-migrate/src/__tests__/address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'

import { parseLibsignalAddressString, signalAddressFromLibsignalString } from '../util/address'

describe('parseLibsignalAddressString', () => {
it('parses Baileys-style dotted address', () => {
assert.deepEqual(parseLibsignalAddressString('5511999999999.0'), {
id: '5511999999999',
device: 0
})
})

it('parses whatsmeow-style colon address', () => {
assert.deepEqual(parseLibsignalAddressString('5511999999999:7'), {
id: '5511999999999',
device: 7
})
})

it('uses the rightmost separator so dotted ids survive', () => {
// Theoretical case — IDs typically don't contain dots, but the parser
// must split at the trailing device delimiter regardless.
assert.deepEqual(parseLibsignalAddressString('1.2.3:42'), {
id: '1.2.3',
device: 42
})
})

it('rejects malformed input', () => {
assert.throws(() => parseLibsignalAddressString('5511'))
assert.throws(() => parseLibsignalAddressString('.5'))
assert.throws(() => parseLibsignalAddressString('5511.'))
assert.throws(() => parseLibsignalAddressString('5511.abc'))
})
})

describe('signalAddressFromLibsignalString', () => {
it('defaults to s.whatsapp.net when no domain marker is present', () => {
const addr = signalAddressFromLibsignalString('5511999999999.0')
assert.equal(addr.user, '5511999999999')
assert.equal(addr.server, 's.whatsapp.net')
assert.equal(addr.device, 0)
})

it('strips Baileys lid suffix and switches server to lid', () => {
const addr = signalAddressFromLibsignalString('102513101521058_1.12')
assert.equal(addr.user, '102513101521058')
assert.equal(addr.server, 'lid')
assert.equal(addr.device, 12)
})

it('honours explicit server override', () => {
const addr = signalAddressFromLibsignalString('5511999999999.3', { server: 'lid' })
assert.equal(addr.server, 'lid')
})

it('does not strip non-numeric underscore suffixes', () => {
const addr = signalAddressFromLibsignalString('user_name.0')
assert.equal(addr.user, 'user_name')
assert.equal(addr.server, 's.whatsapp.net')
})
})
54 changes: 54 additions & 0 deletions packages/store-migrate/src/__tests__/baileys-appstate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'

import { bytesToHex } from 'zapo-js/util'

import { convertBaileysAppStateSyncKey, convertBaileysAppStateVersion } from '../baileys/appstate'

describe('convertBaileysAppStateSyncKey', () => {
it('decodes base64 keyId strings and coerces string timestamps', () => {
const result = convertBaileysAppStateSyncKey('AAECAwQ=', {
keyData: new Uint8Array([10, 20, 30]),
timestamp: '1700000000',
fingerprint: { rawId: 7, currentIndex: 1, deviceIndexes: [0, 12] }
})
assert.deepEqual(Array.from(result.keyId), [0, 1, 2, 3, 4])
assert.deepEqual(Array.from(result.keyData), [10, 20, 30])
assert.equal(result.timestamp, 1700000000)
assert.equal(result.fingerprint?.rawId, 7)
assert.deepEqual(result.fingerprint?.deviceIndexes, [0, 12])
})

it('accepts raw Uint8Array keyId', () => {
const id = new Uint8Array([99])
const result = convertBaileysAppStateSyncKey(id, {
keyData: new Uint8Array([1]),
timestamp: 42
})
assert.equal(result.keyId, id)
assert.equal(result.timestamp, 42)
assert.equal(result.fingerprint, undefined)
})
})

describe('convertBaileysAppStateVersion', () => {
it('rekeys indexValueMap from base64(indexMac) to hex', () => {
const indexMac = new Uint8Array([0xab, 0xcd, 0xef])
const valueMac = new Uint8Array([1, 2, 3])
const indexB64 = Buffer.from(indexMac).toString('base64')

const result = convertBaileysAppStateVersion('regular', {
version: 5,
hash: new Uint8Array(128),
indexValueMap: {
[indexB64]: { valueMac }
}
})

assert.equal(result.collection, 'regular')
assert.equal(result.version, 5)
assert.equal(result.hash.length, 128)
const hexKey = bytesToHex(indexMac)
assert.equal(result.indexValueMap.get(hexKey), valueMac)
})
})
113 changes: 113 additions & 0 deletions packages/store-migrate/src/__tests__/baileys-creds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'

import { proto } from 'zapo-js/proto'

import { convertBaileysCreds } from '../baileys/creds'
import type { BaileysAuthenticationCreds } from '../baileys/types'
import { bufferJsonReviver } from '../util/buffer-json'

// Fixture sourced from the upstream Baileys repo (`baileys_auth_info/creds.json`).
// It is a test/staging account left in the public repo, not user data.
const FIXTURE_TEXT = JSON.stringify({
noiseKey: {
private: { type: 'Buffer', data: '2P6qB8qA7LGQ+YKENJk+ZPLeIiCUE7Ars25d8Lnr7UQ=' },
public: { type: 'Buffer', data: 'DLdrevDQe6DysKKSUl/3IdwYUF2NzAGSyIEpCn/2CRU=' }
},
pairingEphemeralKeyPair: {
private: { type: 'Buffer', data: '4BV6WbXF+gw9AtTmeBEyOX8XGfhjSkZvhF2vfOPT80A=' },
public: { type: 'Buffer', data: 'FF+bnNQwMIlau1LWKYhXMuW69B3hmREeOLD0sxwT9Ek=' }
},
signedIdentityKey: {
private: { type: 'Buffer', data: 'iLPyTWgfLBHuLhJDtahbr6b+xo/VscMkPrxNitQtU3U=' },
public: { type: 'Buffer', data: 'f6MzdnoPtgnaoOz4G7GIKdWNuqjI1W/npxwRDC3T7D4=' }
},
signedPreKey: {
keyPair: {
private: { type: 'Buffer', data: 'sP1nJcLT26EhLlqZ8ieH10jj6+YVZ7O4zbwX6EabaX8=' },
public: { type: 'Buffer', data: 'qF+uRgG/gfuIPAQHTgkPwu/99fBbqPR8fvGfgYIZolk=' }
},
signature: {
type: 'Buffer',
data: 'vxiRzFrRl/7gW5PtqiyfUkDqqPuvSbL511Ay3oVryE8/u/uW6anGIaGbP68zLKxFQbzVPOlGw5H/KJQ0acGZBw=='
},
keyId: 1
},
registrationId: 138,
advSecretKey: 'gr2WI8nNQsCX3T2OFLQ4XEqOFx30KwLoNSyPuG+JBgY=',
processedHistoryMessages: [],
nextPreKeyId: 1,
firstUnuploadedPreKeyId: 1,
accountSyncCounter: 0,
accountSettings: { unarchiveChats: false },
registered: false,
account: {
details: 'CL6U6YIIEIqcr88GGAEgACgA',
accountSignatureKey: 'Bf9vu9HN/FNRTGAM5LkecDimUtgFRJuC0UgCXs37QP9h',
accountSignature:
'Crai7i0IfoU4QpQiHNr8puo1g9NmkhuhexqYx/2FzaJy2ZIjwuaAZzt1sNvbXSwQWYGJdkfguvLIS2uh2fJLBA==',
deviceSignature:
'r7DNs4xAUd4SOT+hKH5wP3F2VAp5pmaS5O0rRx+QjKOvh0/FlCLh765m/bbOiCq0ijD+PX7xPl0dLVYnXZatAg=='
},
me: {
id: '56965746475:[email protected]',
lid: '102513101521058:12@lid'
},
signalIdentities: [
{
identifier: { name: '102513101521058:12@lid', deviceId: 0 },
identifierKey: {
type: 'Buffer',
data: 'Bf9vu9HN/FNRTGAM5LkecDimUtgFRJuC0UgCXs37QP9h'
}
}
],
platform: 'android'
})

describe('convertBaileysCreds (real Baileys fixture)', () => {
it('maps every required zapo field with byte-exact key material', () => {
const fixture = JSON.parse(FIXTURE_TEXT, bufferJsonReviver) as BaileysAuthenticationCreds
const result = convertBaileysCreds(fixture)

// Noise key — round-trip pub/priv unchanged
assert.equal(result.noiseKeyPair.pubKey.length, 32)
assert.equal(result.noiseKeyPair.privKey.length, 32)
assert.equal(
Buffer.from(result.noiseKeyPair.pubKey).toString('base64'),
'DLdrevDQe6DysKKSUl/3IdwYUF2NzAGSyIEpCn/2CRU='
)

// Identity key
assert.equal(result.registrationInfo.registrationId, 138)
assert.equal(result.registrationInfo.identityKeyPair.privKey.length, 32)

// Signed pre-key
assert.equal(result.signedPreKey.keyId, 1)
assert.equal(result.signedPreKey.signature.length, 64)
assert.equal(result.signedPreKey.uploaded, true)

// ADV secret — base64 → 32 bytes
assert.equal(result.advSecretKey.length, 32)
assert.equal(
Buffer.from(result.advSecretKey).toString('base64'),
'gr2WI8nNQsCX3T2OFLQ4XEqOFx30KwLoNSyPuG+JBgY='
)

// Signed identity round-trips through proto encode/decode losslessly
assert.ok(result.signedIdentity)
const encoded = proto.ADVSignedDeviceIdentity.encode(result.signedIdentity).finish()
const decoded = proto.ADVSignedDeviceIdentity.decode(encoded)
assert.equal(decoded.accountSignature?.length, 64)
assert.equal(decoded.deviceSignature?.length, 64)
assert.equal(decoded.accountSignatureKey?.length, 33)

// Identity propagation
assert.equal(result.meJid, '56965746475:[email protected]')
assert.equal(result.meLid, '102513101521058:12@lid')
assert.equal(result.platform, 'android')

// Server prekey state — fresh account, none uploaded yet
assert.equal(result.serverHasPreKeys, false)
})
})
Loading
Loading