0x0-cli 1.0.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/bin/0x0.js +141 -0
- package/dist/web/assets/index-BrxSM83h.css +1 -0
- package/dist/web/assets/index-Ci89CtgQ.js +176 -0
- package/dist/web/index.html +15 -0
- package/package.json +37 -0
- package/src/commands/chat.js +204 -0
- package/src/commands/contact.js +63 -0
- package/src/commands/inbox.js +68 -0
- package/src/commands/init.js +31 -0
- package/src/commands/listen.js +74 -0
- package/src/commands/pin.js +114 -0
- package/src/commands/pipe.js +121 -0
- package/src/commands/qr.js +35 -0
- package/src/commands/read.js +48 -0
- package/src/commands/renew.js +28 -0
- package/src/commands/send.js +61 -0
- package/src/commands/web.js +37 -0
- package/src/commands/whoami.js +24 -0
- package/src/core/channel.js +10 -0
- package/src/core/identity.js +8 -0
- package/src/core/message.js +35 -0
- package/src/core/pin.js +10 -0
- package/src/core/swarm.js +12 -0
- package/src/core/uri.js +12 -0
- package/src/storage/contacts.js +72 -0
- package/src/storage/identity.js +30 -0
- package/src/storage/messages.js +48 -0
- package/src/storage/pins.js +71 -0
- package/src/web/api.js +375 -0
- package/src/web/server.js +52 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import * as identityStore from '../storage/identity.js'
|
|
3
|
+
import * as pinsStore from '../storage/pins.js'
|
|
4
|
+
|
|
5
|
+
export async function cmdWhoami() {
|
|
6
|
+
const identity = identityStore.load()
|
|
7
|
+
if (!identity) {
|
|
8
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
9
|
+
process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log(chalk.gray('// my_number'))
|
|
13
|
+
console.log(chalk.white(identity.number))
|
|
14
|
+
console.log()
|
|
15
|
+
|
|
16
|
+
const pins = pinsStore.getActive()
|
|
17
|
+
if (pins.length > 0) {
|
|
18
|
+
console.log(chalk.gray('// active_pins'))
|
|
19
|
+
for (const p of pins) {
|
|
20
|
+
const label = p.label ? chalk.white(p.label) : chalk.gray('(no label)')
|
|
21
|
+
console.log(` ${chalk.hex('#aaaaaa')(p.value)} ${label}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
|
|
3
|
+
const APP_SALT = '0x0-v1-2026'
|
|
4
|
+
|
|
5
|
+
// チャンネルシークレット(Hyperswarm topic)
|
|
6
|
+
// 受信者の番号とPINからsha256で32バイトのトピックIDを生成
|
|
7
|
+
export function channelSecret(recipientNumber, pin) {
|
|
8
|
+
const input = `0x0:${recipientNumber}:${pin}:${APP_SALT}`
|
|
9
|
+
return crypto.createHash('sha256').update(input).digest()
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// 番号生成: 0x0-NNN-NNNN-NNNN
|
|
2
|
+
|
|
3
|
+
export function generateNumber() {
|
|
4
|
+
const seg3 = String(Math.floor(Math.random() * 900) + 100)
|
|
5
|
+
const seg4a = String(Math.floor(Math.random() * 9000) + 1000)
|
|
6
|
+
const seg4b = String(Math.floor(Math.random() * 9000) + 1000)
|
|
7
|
+
return `0x0-${seg3}-${seg4a}-${seg4b}`
|
|
8
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// メッセージフォーマット(モバイル版と共通)
|
|
2
|
+
|
|
3
|
+
export function createMessage(content) {
|
|
4
|
+
return JSON.stringify({
|
|
5
|
+
version: '1',
|
|
6
|
+
type: 'message',
|
|
7
|
+
content,
|
|
8
|
+
timestamp: Date.now()
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createFileMessage(filename, mimeType, dataBase64) {
|
|
13
|
+
return Buffer.from(JSON.stringify({
|
|
14
|
+
version: '1',
|
|
15
|
+
type: 'file',
|
|
16
|
+
filename,
|
|
17
|
+
mimeType,
|
|
18
|
+
data: dataBase64,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createPing() {
|
|
24
|
+
return JSON.stringify({ version: '1', type: 'ping' })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseMessage(raw) {
|
|
28
|
+
try {
|
|
29
|
+
const msg = JSON.parse(raw.toString())
|
|
30
|
+
if (!msg.version || !msg.type) return null
|
|
31
|
+
return msg
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/core/pin.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Hyperswarm from 'hyperswarm'
|
|
2
|
+
import { channelSecret } from './channel.js'
|
|
3
|
+
|
|
4
|
+
export function createSwarm() {
|
|
5
|
+
return new Hyperswarm()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 相手の番号とPINでチャンネルに参加
|
|
9
|
+
export function joinChannel(swarm, recipientNumber, pin, opts = {}) {
|
|
10
|
+
const topic = channelSecret(recipientNumber, pin)
|
|
11
|
+
return swarm.join(topic, { server: true, client: true, ...opts })
|
|
12
|
+
}
|
package/src/core/uri.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// 0x0://NUMBER/PIN
|
|
2
|
+
const URI_RE = /^0x0:\/\/(0x0-\d+-\d+-\d+)\/(.+)$/
|
|
3
|
+
|
|
4
|
+
export function parseUri(uri) {
|
|
5
|
+
const m = URI_RE.exec(uri)
|
|
6
|
+
if (!m) return null
|
|
7
|
+
return { number: m[1], pin: m[2] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildUri(number, pin) {
|
|
11
|
+
return `0x0://${number}/${pin}`
|
|
12
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
5
|
+
|
|
6
|
+
const FILE = path.join(os.homedir(), '.0x0', 'contacts.json')
|
|
7
|
+
|
|
8
|
+
export function loadAll() {
|
|
9
|
+
if (!fs.existsSync(FILE)) return []
|
|
10
|
+
try { return JSON.parse(fs.readFileSync(FILE, 'utf8')) } catch { return [] }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function saveAll(contacts) {
|
|
14
|
+
fs.writeFileSync(FILE, JSON.stringify(contacts, null, 2), { mode: 0o600 })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function create({ theirNumber, theirPin, label = '', peerPublicKey = null }) {
|
|
18
|
+
const contacts = loadAll()
|
|
19
|
+
const existing = contacts.find(c => c.theirNumber === theirNumber && c.theirPin === theirPin)
|
|
20
|
+
if (existing) {
|
|
21
|
+
if (peerPublicKey && !existing.peerPublicKey) {
|
|
22
|
+
existing.peerPublicKey = peerPublicKey
|
|
23
|
+
saveAll(contacts)
|
|
24
|
+
}
|
|
25
|
+
return existing
|
|
26
|
+
}
|
|
27
|
+
const contact = {
|
|
28
|
+
id: uuidv4(),
|
|
29
|
+
theirNumber,
|
|
30
|
+
theirPin,
|
|
31
|
+
label,
|
|
32
|
+
peerPublicKey,
|
|
33
|
+
createdAt: Date.now()
|
|
34
|
+
}
|
|
35
|
+
contacts.push(contact)
|
|
36
|
+
saveAll(contacts)
|
|
37
|
+
return contact
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function matchId(contact, id) {
|
|
41
|
+
return contact.id === id || contact.id.startsWith(id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function findById(id) {
|
|
45
|
+
return loadAll().find(c => matchId(c, id)) || null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function findByPublicKey(hexStr) {
|
|
49
|
+
return loadAll().find(c => c.peerPublicKey === hexStr) || null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function update(id, changes) {
|
|
53
|
+
const contacts = loadAll()
|
|
54
|
+
const idx = contacts.findIndex(c => matchId(c, id))
|
|
55
|
+
if (idx === -1) return null
|
|
56
|
+
Object.assign(contacts[idx], changes)
|
|
57
|
+
saveAll(contacts)
|
|
58
|
+
return contacts[idx]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function updateLabel(id, label) {
|
|
62
|
+
return update(id, { label })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function updatePublicKey(id, hexStr) {
|
|
66
|
+
return update(id, { peerPublicKey: hexStr })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function remove(id) {
|
|
70
|
+
const contacts = loadAll().filter(c => !matchId(c, id))
|
|
71
|
+
saveAll(contacts)
|
|
72
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
|
|
5
|
+
const DIR = path.join(os.homedir(), '.0x0')
|
|
6
|
+
const FILE = path.join(DIR, 'identity.json')
|
|
7
|
+
|
|
8
|
+
export function ensureDir() {
|
|
9
|
+
if (!fs.existsSync(DIR)) {
|
|
10
|
+
fs.mkdirSync(DIR, { recursive: true, mode: 0o700 })
|
|
11
|
+
}
|
|
12
|
+
const msgDir = path.join(DIR, 'messages')
|
|
13
|
+
if (!fs.existsSync(msgDir)) {
|
|
14
|
+
fs.mkdirSync(msgDir, { recursive: true })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function load() {
|
|
19
|
+
if (!fs.existsSync(FILE)) return null
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(FILE, 'utf8'))
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function save(data) {
|
|
28
|
+
ensureDir()
|
|
29
|
+
fs.writeFileSync(FILE, JSON.stringify(data, null, 2), { mode: 0o600 })
|
|
30
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
5
|
+
import { hashPin } from '../core/pin.js'
|
|
6
|
+
|
|
7
|
+
const BASE = path.join(os.homedir(), '.0x0', 'messages')
|
|
8
|
+
|
|
9
|
+
function dirForPin(pinValue) {
|
|
10
|
+
return path.join(BASE, hashPin(pinValue))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function logFile(pinValue) {
|
|
14
|
+
return path.join(dirForPin(pinValue), 'log.jsonl')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function append(pinValue, { from, content, isMine }) {
|
|
18
|
+
const dir = dirForPin(pinValue)
|
|
19
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
20
|
+
const entry = JSON.stringify({
|
|
21
|
+
id: uuidv4(),
|
|
22
|
+
from,
|
|
23
|
+
content,
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
isMine
|
|
26
|
+
})
|
|
27
|
+
fs.appendFileSync(logFile(pinValue), entry + '\n')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function list(pinValue, limit = 100) {
|
|
31
|
+
const file = logFile(pinValue)
|
|
32
|
+
if (!fs.existsSync(file)) return []
|
|
33
|
+
return fs.readFileSync(file, 'utf8')
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map(line => { try { return JSON.parse(line) } catch { return null } })
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.slice(-limit)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getLatest(pinValue) {
|
|
42
|
+
const msgs = list(pinValue, 1)
|
|
43
|
+
return msgs[msgs.length - 1] || null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function countUnread(pinValue) {
|
|
47
|
+
return list(pinValue).filter(m => !m.isMine).length
|
|
48
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
5
|
+
import { hashPin } from '../core/pin.js'
|
|
6
|
+
|
|
7
|
+
const FILE = path.join(os.homedir(), '.0x0', 'pins.json')
|
|
8
|
+
|
|
9
|
+
export function loadAll() {
|
|
10
|
+
if (!fs.existsSync(FILE)) return []
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(FILE, 'utf8'))
|
|
13
|
+
} catch {
|
|
14
|
+
return []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveAll(pins) {
|
|
19
|
+
fs.writeFileSync(FILE, JSON.stringify(pins, null, 2), { mode: 0o600 })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function create({ value, label = '', expiry = 'none', expiresAt = null }) {
|
|
23
|
+
const pins = loadAll()
|
|
24
|
+
const pin = {
|
|
25
|
+
id: uuidv4(),
|
|
26
|
+
value,
|
|
27
|
+
valueHash: hashPin(value),
|
|
28
|
+
label,
|
|
29
|
+
expiry,
|
|
30
|
+
expiresAt,
|
|
31
|
+
createdAt: Date.now(),
|
|
32
|
+
isActive: true
|
|
33
|
+
}
|
|
34
|
+
pins.push(pin)
|
|
35
|
+
saveAll(pins)
|
|
36
|
+
return pin
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findByValue(value) {
|
|
40
|
+
return loadAll().find(p => p.value === value && p.isActive) || null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function findById(id) {
|
|
44
|
+
return loadAll().find(p => p.id === id) || null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function update(id, changes) {
|
|
48
|
+
const pins = loadAll()
|
|
49
|
+
const idx = pins.findIndex(p => p.id === id)
|
|
50
|
+
if (idx === -1) return null
|
|
51
|
+
pins[idx] = { ...pins[idx], ...changes }
|
|
52
|
+
saveAll(pins)
|
|
53
|
+
return pins[idx]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function rotate(id, newValue) {
|
|
57
|
+
return update(id, { value: newValue, valueHash: hashPin(newValue) })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function revoke(id) {
|
|
61
|
+
return update(id, { isActive: false })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getActive() {
|
|
65
|
+
const now = Date.now()
|
|
66
|
+
return loadAll().filter(p => {
|
|
67
|
+
if (!p.isActive) return false
|
|
68
|
+
if (p.expiresAt && p.expiresAt < now) return false
|
|
69
|
+
return true
|
|
70
|
+
})
|
|
71
|
+
}
|