const e = require("keet-identity-key"),
{ ProofEncoding: t } = require("keet-identity-key/lib/encoding"),
i = require("hypercore-storage"),
s = require("hypercore-id-encoding"),
r = require("ready-resource"),
o = require("ready-guard"),
a = require("hyperswarm"),
n = require("hyperdht"),
l = require("corestore"),
c = require("hypercore"),
h = require("hyperblobs"),
d = require("autobase"),
y = require("scope-lock"),
u = require("rache"),
p = require("protomux-wakeup"),
m = require("promaphore"),
g = require("safety-catch"),
b = require("task-backoff"),
w = require("hypercore-crypto"),
f = require("compact-encoding"),
v = require("z32"),
k = require("b4a"),
P = require("fs"),
K = require("path"),
_ = require("blind-peering"),
T = require("multi-profile-store"),
R = require("hyperconf")
;(c.enable(c.SMALL_WANTS), c.setRecoveryPeers && c.setRecoveryPeers([]))
const L = require("@holepunchto/keet-push"),
E = require("@holepunchto/keet-unread-anchors"),
{ generateMnemonic: C } = require("bip39-mnemonic"),
{ HyperDiscoveryClient: B } = require("@holepunchto/hyperdiscovery"),
x = require("@holepunchto/keet-indexing"),
D = require("./schema/versions"),
{ FEATURES: I } = D,
S = require("./lib/api/v1"),
A = require("./lib/api/v2"),
O = require("./lib/core/crypto"),
q = require("./lib/core/local"),
V = require("./lib/core/files"),
M = require("./lib/core/tasks"),
U = require("./lib/core/sync"),
N = require("./lib/core/migrations/3/apply"),
{ SwarmWorker: F } = require("./lib/core/swarm"),
{
getWriterKey: W,
getInvitationId: j,
createInvitation: $,
generateRoomCapabilities: G,
generateIdentityPairingName: H,
generateRoomPairingName: J,
} = require("./lib/core/invitations"),
{ getMemberForDeviceKey: Y } = require("./lib/core/contract/query"),
{ restoreAvatar: z } = require("./lib/core/contract/avatars"),
{ RoomStatus: Q } = require("./lib/core/contract/identity/private"),
{ DEBUG_FLAGS: X, DHT_SETTINGS: Z } = require("./lib/core/debug/debugging"),
{ KeetContract: ee } = require("./lib/core/contract"),
{ KeetContractError: te } = require("./lib/core/contract/errors.js"),
ie = require("./lib/core/log"),
{
contract: {
hyperdispatch: { decode: se, DispatchError: re },
hyperschema: { getEnum: oe, getEncoding: ae, encode: ne, decode: le },
},
local: {
hyperschema: { getEnum: ce },
},
config: { hyperconf: he },
} = require("./schema/spec"),
de = ae("@keet-private-id/profile-discovery-record"),
ye = oe("@keet/build-type"),
ue = require("./lib/core/users"),
pe = require("protomux-rpc-client"),
{ KeetErrorManager: me, KeetErrorV2: ge } = require("./lib/errors"),
{ Version: be } = require("bare-semver"),
{ roomMigrationsStats: we } = require("./lib/core/contract/stats/local"),
fe = require("./lib/bundle"),
ve = ce("@local/keet-state"),
ke = 2592e6,
Pe = "keet-swarm-keypair",
Ke = "keet-core",
_e = "keet-unreads",
Te = "keet-bundles",
Re = 1e4,
Le = 3e3,
Ee = 4,
Ce = 5e3,
Be = k.alloc(32),
xe = 1,
De = 500,
Ie = 9,
Se = 1728e5,
Ae = 5,
Oe = 10,
qe = 1711547109544,
Ve = require("./lib/env"),
Me = 64,
Ue = 3,
Ne = 30,
Fe = 4
class We {
constructor(e, t) {
;((this.keet = e),
(this.roomId = t),
(this.batch = null),
(this.length = 0),
(this.timestamp = 0),
(this.pushLength = 0))
}
get key() {
return this.room.base.local.key
}
_open() {
;((this.room = this.keet.internalOpenExistingRoom(this.roomId)),
(this.batch = this.room.base.batch()),
(this.timestamp = Date.now()))
}
_noop() {
return (this.close(), this.length)
}
async dispatch(e, t, { dryRun: i = !0 } = {}) {
;(this.room || this._open(), this.room.opened || (await this.room.ready()))
const s = await this.room._internalEncodeOperation(e, t, {
view: this.room.view,
timestamp: this.timestamp,
})
return i && !(await this.keet.internalDryRun(this.room, this.room.view, [s]))
? this._noop()
: ((this.length = await this.append(s)),
!this.pushLength && Je(e) && (this.pushLength = this.length),
this.length)
}
async append(e) {
return this.batch.append(e)
}
async flush() {
try {
return (
await this.batch.flush(),
await this.room._postAppend({
encoded: this.batch.blocks,
pushLength: this.pushLength,
timestamp: this.timestamp,
}),
{ key: this.key, length: this.length }
)
} finally {
this.close()
}
}
close() {
;(this.room && this.keet.internalReleaseRoom(this.room),
this.batch && this.batch.close(),
(this.batch = null),
(this.length = 0),
(this.shouldPush = !1))
}
}
class je extends r {
constructor(e, t, i, r, o) {
;(super(),
(this.keet = e),
(this.id = t),
(this.key = i),
(this.base = o),
(this.externalBlobs = null),
(this.discoveryKey = r),
(this.discoveryId = s.encode(this.discoveryKey)),
(this.view = this.base.view),
(this.db = this.view.db),
(this.core = this.view.core),
(this.blobs = this.view.blobs),
(this.router = this.view.router),
(this.push = null),
this.keet.push && (this.push = this.keet.push.room(this.key, this.discoveryKey)),
(this.refs = 0),
(this.idleTicks = 0),
(this._pushLock = new y({ debounce: !0 })),
this.ready().catch(ze))
}
get deviceId() {
return s.encode(this.deviceKey)
}
get deviceKey() {
return this.base.local.key
}
get encryptionKey() {
return this.base.encryptionKey
}
getQuickStats() {
return {
view: {
peers: this.view.core.peers.length,
length: this.view.core.length,
signedLength: this.view.core.signedLength,
byteLength: this.view.core.byteLength,
},
system: {
peers: this.base.core.peers.length,
length: this.base.length,
signedLength: this.base.signedLength,
indexedLength: this.base.indexedLength,
},
base: {
opened: this.base.opened,
closed: this.base.closed,
opening: !!this.base.opening,
closing: !!this.base.closing,
writable: this.base.writable,
indexing: this.base.ackable,
interrupted: !!this.base.interrupted,
appending: !!this.base.appending,
flushing: !!this.base.flushing,
advancing: !!this.base._advancing,
},
}
}
async getStats() {
const [e, t, i] = await Promise.all([
this.keet.local.get("@local/rooms", { roomKey: this.key }),
this.keet.local.get("@local/room-preferences", { roomKey: this.key }),
this.keet.local.get("@local/writers-by-room-key", { roomKey: this.key }),
]),
{ view: r, system: o, base: a } = this.getQuickStats()
return {
view: r,
system: o,
base: a,
writer: {
key: i.key ? s.encode(i.key) : null,
roomKey: i?.roomKey ? s.encode(i.roomKey) : null,
},
status: e?.status,
preferences: t?.status,
}
}
async _createDeviceAttestation(e, t) {
const i = await this.keet.internalEnterLocalTransaction()
try {
const s = this.keet.internalAttestData(e, t)
return (
await i.insert("@local/device-attestations", {
deviceKey: e,
proof: s,
createdAt: Date.now(),
}),
await i.flush(),
s
)
} finally {
await this.keet.internalExitLocalTransaction()
}
}
async _attestedPushNotify(e) {
const t = this.deviceKey,
[i, s] = await Promise.all([
this.keet.local.get("@local/identity"),
this.keet.local.get("@local/device-attestations", { deviceKey: t }),
]),
r = s?.createdAt || 0,
o = i.proofShortenedAt || 0,
a = !s || r < o ? await this._createDeviceAttestation(t, i) : s.proof
await this.push.send(this.base.local, e, { version: 1, attest: a })
}
async _safePushNotify(e) {
if (
(this._pushLengths || (this._pushLengths = []),
this._pushLengths.push(e),
!(await this._pushLock.lock()))
)
return
if (this.closing) return
const t = this._pushLengths
this._pushLengths = null
try {
await this._attestedPushNotify(t)
} catch (e) {
this.keet.onerror(e, { roomKey: this.key })
}
this._pushLock.unlock()
}
async _safeBumpDiscovery(e) {
try {
const t = await this.keet.local.get("@local/rooms", { roomKey: this.key })
this.keet.discovery.announce(t.capabilities.discoveryKeyPair, { bump: e })
} catch (e) {
this.keet.onerror(e, { roomKey: this.key })
}
}
async _open() {
;(await this.base.ready(),
this.internalEnsureMirroring(),
this.base.on("writer", async (e) => {
try {
if (await this.keet.local.get("@local/devices", { device: { key: e.core.key } })) return
const t = await this.db.get("@keet/devices", { key: e.core.key })
t && (await this.db.get("@keet/members", { key: t.memberKey }))
} catch (e) {
g(e)
}
}),
this.base.local.on("remote-contiguous-length", this._handleSentLengthChanged.bind(this)),
this.base.on("rotate-local-writer", () => {
this.base.local.on("remote-contiguous-length", this._handleSentLengthChanged.bind(this))
}),
this.db.get("@keet/room-config").then((e) => {
this.keet.logger.debug("opened id: " + this.id + " title: " + e?.title)
}, g),
(this.externalBlobs = new h(
this.keet.store.get({
name: this.id + "/blobs",
encryptionKey: this.encryptionKey,
encrypted: !0,
exclusive: !0,
active: !1,
}),
)),
await this.externalBlobs.ready(),
await this._ensureMetadata(),
this.internalEnsureProfileUpdated(),
this.internalEnsureUpgraded(),
this.internalEnsureRepaired())
}
async internalCloseBase() {
;(this.keet.internalTime(`internal-close-base-${this.id}`),
await this.base.close(),
this.keet.internalTimeEnd(`internal-close-base-${this.id}`))
}
async internalCloseExternalBlobs() {
;(this.keet.internalTime(`internal-close-blobs-${this.id}`),
await this.externalBlobs.close(),
this.keet.internalTimeEnd(`internal-close-blobs-${this.id}`))
}
async internalPushDestroy() {
this.push &&
(this.keet.internalTime(`internal-close-push-${this.id}`),
await this.push.destroy(),
this.keet.internalTimeEnd(`internal-close-push-${this.id}`))
}
_close() {
return Promise.all([
this.internalCloseBase(),
this.internalCloseExternalBlobs(),
this.internalPushDestroy(),
])
}
_handleSentLengthChanged() {
if (!this.base.writable) return
const { length: e, remoteContiguousLength: t } = this.base.local
e === t && this.keet.emit("room-sent-message", this.base.id)
}
async _ensureMetadata() {
const e = await this.keet.internalEnterLocalTransaction()
try {
const [t, i] = await Promise.all([
e.get("@local/room-metadata", { roomKey: this.key }),
e.get("@local/rooms", { roomKey: this.key }),
])
if (t) return
const s = i ? 0 : 1
;(await e.insert("@local/room-metadata", { roomKey: this.key, repairs: s }), await e.flush())
} finally {
await this.keet.internalExitLocalTransaction()
}
}
async internalEnsureMirroring() {
this.refs++
try {
const e = this.base.wakeupCapability && this.base.wakeupCapability.key,
t = await this.keet._getMirrorKeys(this.key)
if (this.closing) return
this.keet.blindPeering.addAutobaseBackground(this.base, { target: e, keys: t })
} finally {
this.refs--
}
}
async internalEnsureUpgraded({ opening: e = !1 } = {}) {
this.refs++
try {
e || this.keet.onlineBoot.opened || (await this.keet.onlineBoot.ready())
const [t, i] = await Promise.all([
this.db.get("@keet/system-digest"),
Y(this.db, this.deviceKey),
])
if (!i?.status.isModerator || t?.abi.version >= this.keet.abi.autoupgrade) return
await this.keet.internalDispatchUpgrade(this.id, this.keet.abi.autoupgrade)
} catch (e) {
this.keet.onerror(e, { roomKey: this.key })
} finally {
this.refs--
}
}
async internalEnsureProfileUpdated() {
this.refs++
try {
this.keet.onlineBoot.opened || (await this.keet.onlineBoot.ready())
const [e, t] = await Promise.all([
this.keet.local.get("@local/identity"),
Y(this.db, this.deviceKey),
])
if (this.closing || !e || !t || k.equals(this.key, e.privateProfileKey)) return
const i = await this.keet.local.get("@local/rooms", { roomKey: e.privateProfileKey })
if (this.closing) return
if (t.profile.timestamp >= i.selfMember.profile.timestamp) return
const s = {
profile: i.selfMember.profile,
attestation: { memberKey: i.selfMember.key, proof: i.selfMember.proofs.profileAttestation },
}
await this.dispatch("@keet/update-member", { member: s })
} catch (e) {
this.keet.onerror(e, { roomKey: this.key })
} finally {
this.refs--
}
}
async internalEnsureRepaired() {
this.refs++
try {
if (
(this.keet.onlineBoot.opened || (await this.keet.onlineBoot.ready()),
this === this.keet.privateProfile)
)
return
const e = await this.keet.local.get("@local/room-metadata", { roomKey: this.key })
if (this.closing || e.repairs >= 1) return
if (this.base.linearizer.size > De) return
const t = await this.db.get("@keet-private-id/identity")
if (this.closing || t) return
if (((e.repairs = 1), await this.base.repair(), this.closing)) return
const i = await this.keet.internalEnterLocalTransaction()
try {
;(await i.insert("@local/room-metadata", e), await i.flush())
} finally {
await this.keet.internalExitLocalTransaction()
}
} finally {
this.refs--
}
}
async _internalEncodeOperation(e, t, { view: i = this.view, timestamp: s = Date.now() } = {}) {
const r = await i.db.get("@keet/system-digest")
if (!this.keet.isValidBuildType(r)) throw ge.INVALID_BUILD_TYPE()
return (
await this.keet.contractBundles.loadBundle(r ? r.abi.version : this.keet.abi.autoupgrade)
).encodeOperation(e, { header: { timestamp: s }, ...t })
}
async _encodeBatch(e, t = {}) {
const i = Array.isArray(e) ? e : [e],
s = Date.now(),
r = []
let o = !1
for (const { type: e, op: a } of i)
(r.push(this._internalEncodeOperation(e, a, { ...t, timestamp: s })), Je(e) && (o = !0))
return { encoded: await Promise.all(r), timestamp: s, shouldPush: o }
}
async _postAppend({ encoded: e = [], pushLength: t = 0, timestamp: i = Date.now() }) {
;((this.keet.stats.ops += e.length),
this.push && t > 0 && this._safePushNotify(t),
this.keet.discovery && this._safeBumpDiscovery(i),
this.db.update())
}
async dispatchBatch(e, { view: t = this.view, dryRun: i = !0, optimistic: s = !1 } = {}) {
this.opened || (await this.ready())
const { encoded: r, timestamp: o, shouldPush: a } = await this._encodeBatch(e, { view: t })
if (i && !(await this.keet.internalDryRun(this, t, r))) return null
const n = await this.base.append(r, { optimistic: s }),
l = a ? n : 0
return (
await this._postAppend({ encoded: r, pushLength: l, timestamp: o }),
{ key: this.base.local.key, length: n }
)
}
dispatch(e, t, i) {
return this.dispatchBatch([{ type: e, op: t }], i)
}
}
module.exports = class extends r {
constructor(e, t = {}) {
if (
(super(),
(this.dir = e),
(this.opts = t),
(this.isMobile = !!t.mobile),
(this.isTesting = !!t.testing),
(this.appVersion = t.appVersion ? be.parse(t.appVersion) : null),
(this.forceBuildType = !!t.forceBuildType),
!t.buildType)
)
throw ge.INVALID_BUILD_TYPE()
;((this.buildType = Ye(t.buildType)),
(this.logger = new ie(t.logger)),
this.isTesting && this.logger.mute())
const s = Ve(t.buildType)
;((this.isNightly = this.buildType === ye.Nightly),
(this.isExperimental = this.isTesting ? (t.experimental ?? this.isNightly) : this.isNightly),
(this.offlineBoot = new o()),
(this.onlineBoot = new o()),
(this.debug = {}),
(this.versions = D),
(this.abi = t.testingABI || {
max: this.isExperimental ? D.EXPERIMENTAL : D.STABLE,
autoupgrade: this.isExperimental ? D.EXPERIMENTAL : D.AUTOUPGRADE,
production: D.STABLE,
}),
(this.logout = t.logout),
(this.storage = new i(e, t.storage)),
(this.rootCorestore = new l(this.storage, {
globalCache: new u({ maxSize: t.maxCacheSize }),
manifestVersion: 2,
})),
(this.store = this.rootCorestore.namespace(Ke)),
this.rootCorestore.watch(this._handleCoreOpen.bind(this)),
(this.local = q.rocks(this.storage.rocks.columnFamily("keet/local-database"))),
(this.discovery = null),
(this.gatekeepers = t.gatekeepers || []),
(this.push = null),
(this.protomuxWakeup = new p(this._handleWakeup.bind(this))),
(this.backoff = new b()),
(this.rooms = new Map()),
(this.activeRooms = new Set()),
(this._transactionLock = new y()),
(this._tx = null),
(this.hyperconf = null),
(this.config = null),
(this.configMinVersion = null),
(this.errorManager = new me(this, this.opts)),
(this.swarmWorker = new F(this, this.opts.swarmWorker)),
(this.files = new V(this, this.opts)),
(this._onerror = t.onerror || g),
(this.onerror = this.errorManager
? (e, t = {}) => {
const i = this.errorManager.normalize(e, t)
;(this.errorManager.add(i), this.logger.error(i), this._onerror(i))
}
: this._onerror),
(this.apiV1 = new S(this, t)),
(this.backendAPI = this.apiV1),
(this.api = this.backendAPI.api.core),
(this.blindPeeringOpts = t.blindPeeringOpts || {}),
(this.contractBundles = null),
(this.tasks = null),
(this.swarm = null),
(this.swarmId = null),
(this.blindPeering = null),
(this.privateProfile = null),
(this.unreads = null),
(this.unreadsLoading = null),
(this.discoveryCore = null),
(this.discoveryCoreSession = null),
(this.push = null),
(this.state = ve.RESUME),
(this._bundlesKey = t.bundlesKey || s.bundles.key),
(this._bundlesLength = t.bundlesLength || s.bundles.length),
(this._hyperconfKey = t.hyperconfKey || s.config.key),
(this._hyperconfLength = t.hyperconfLength || s.config.length),
(this._disabledRooms = this.isTesting ? new Set() : null),
(this._verifiedRooms = this.isTesting ? new Set() : null),
(this._relayKeys = this.isTesting ? t.blindRelays || [] : null),
(this._blindPeerKeys = this.isTesting ? t.blindMirroring || [] : null),
(this._gatekeeperKeys = this.isTesting ? t.gatekeepers || [] : null),
(this._discoveryBootstrapKeys = this.isTesting ? t.discoveryBootstrapKeys || [] : null),
(this._disableUnreadSync = (this.isTesting && t.disableUnreadSync) || !1),
(this._otaResyncPrivateProfileBump = (this.isTesting && t.otaResyncPrivateProfileBump) || 0),
(this._migratedFrom = 0),
(this._unreadsLock = new y({ debounce: !0 })),
(this._dhtOpts = t.dht),
(this._swarmOpts = t.swarm),
(this._pendingTasks = []),
(this._swarmTasks = []),
(this._runningTasks = new Set()),
(this._waitingForCompletion = new Map()),
(this._completedRequests = new Set()),
(this._pendingCandidates = new Map()),
(this._swarming = new Map()),
(this._pendingTraces = []),
(this._cacheLock = new y({ debounce: !0 })),
(this._gcEnabled = !1 !== t.gc),
(this._gcIdleTicks = t.gcIdleTicks || 4),
(this._gcInterval = t.gcInterval || Ce),
(this._gcTimer = null),
(this._gcClosingRooms = new Set()),
(this._boundAutobaseTrace = this._autobaseTrace.bind(this)),
(this.internalPairingTimeout = t.pairingTimeout || Le),
(this._profileDiscoveryTimeout = t.profileDiscoveryTimeout || Re),
(this.migrationStatus = null),
(this._roomMigrations = new m(5)),
(this._discoveryLock = new y({ debounce: !0 })),
(this._discoverySemaphore = new m(Oe)),
(this._discovering = new Map()),
(this.internalFastJoinSemaphore = new m(4)),
(this.debugRunningFastJoins = 0),
(this.debugMaxRunningFastJoins = 0),
(this._rejectAnonymousProfiles = !1 !== t.rejectAnonymousProfiles),
(this._registryOpts = this.isTesting ? t.userRegistry : null),
(this.rpcClient = null),
(this.userRegistry = null),
(this.ackThreshold = t.ackThreshold),
(this.ackInterval = t.ackInterval),
(this.stats = {
ops: 0,
appends: 0,
bytesAppended: 0,
uploaded: 0,
bytesUploaded: 0,
downloaded: 0,
bytesDownloaded: 0,
}),
(this.debugForceMigrationFailure = t.debugForceMigrationFailure || null),
(this.debugJoinPeer = t.debugJoinPeer || this._debugJoinPeer.bind(this)),
(this.debugLeavePeer = t.debugLeavePeer || this._debugLeavePeer.bind(this)),
this.ready().catch(this.onerror))
}
async backendReady() {
this.backendAPI.opened || (await this.backendAPI.ready())
}
get readOnly() {
return this.rootCorestore.storage.readOnly || !1
}
get suspended() {
return this.state === ve.SUSPEND
}
async _openNetworking() {
const e = await this.getDHTSettings()
;((this.swarm = new a({
deferRandomPunch: !0,
dht: new n({ ...e, ...this._swarmOpts, ...this._dhtOpts }),
maxPeers: Ne,
...this._swarmOpts,
relayThrough: this._relayThrough.bind(this),
keyPair: await this.rootCorestore.createKeyPair(Pe),
})),
(this.swarmId = s.encode(this.swarm.keyPair.publicKey)),
this.swarm.on("connection", this._handleConnection.bind(this)),
(this.discovery = this._discoveryBootstrapKeys.length
? new B(this._discoveryBootstrapKeys[0], { dht: this.swarm.dht })
: null),
(this.rpcClient = new pe(this.swarm.dht, {
suspended: this.state === ve.SUSPEND,
relayThrough: this._relayThrough.bind(this),
})),
(this.blindPeering = new _(this.swarm.dht, this.store, {
pick: 2,
wakeup: this.protomuxWakeup,
keys: this.debug.disableMirrors ? [] : this._blindPeerKeys,
relayThrough: this._relayThrough.bind(this),
...this.blindPeeringOpts,
})),
this._gatekeeperKeys.length > 0 &&
(this.push = new L(this._gatekeeperKeys, { dht: this.swarm.dht })),
await this.swarmWorker.ready(),
await this.files.ready(),
this.readOnly && (this.blindPeering.suspend(), this.swarm.suspend()))
}
async _runBootOffline() {
if (!this.offlineBoot.enter()) return
;(await this._ensureDebugState(),
await this.store.ready(),
(this.hyperconf = new R(he, this.store.get(this._hyperconfKey), {
minLength: this._hyperconfLength,
production: this.buildType === ye.Production,
})),
this.hyperconf.on("update", this._handleConfigUpdated.bind(this)),
await this.hyperconf.ready(),
await this._handleConfigUpdated(),
this.config?.versions.autoupgrade > 0 &&
(this.abi.autoupgrade = this.config.versions.autoupgrade))
const e = await this.local.get("@local/environment"),
t = !!e && e.latestOfflineAbi < this.abi.production
;((this.contractBundles = new fe(this, Te, { hydrate: t })),
await this.contractBundles.ready(),
t && (await this._bumpLocalAbiVersion()),
await this._openNetworking(),
(this.tasks = new M(this)),
await this.tasks.ready(),
this._gcEnabled &&
(this._gcTimer = setInterval(this._closeIdleRooms.bind(this), this._gcInterval)),
this.offlineBoot.exit())
}
async _runBootOnline() {
if (this.onlineBoot.enter()) {
;(await this.offlineBoot.ready(),
this.discovery &&
(this.discovery.on("announce", ({ publicKey: e }) => {
this.scheduleDiscoverySwarming(e)
}),
this.discovery.on("connect", this._onDiscoveryConnect.bind(this)),
this.discovery.connect()),
this._refreshDiscoverySwarming(),
(this._migratedFrom = await this._ensureMigrated()))
try {
await this._loadIdentity()
} catch (e) {
this.onerror(e)
}
;(this._migratedFrom > 0 && (await this._maybeRunPostMigrations(this._migratedFrom)),
this._registryOpts &&
((this.userRegistry = new ue(this, this._registryOpts)), await this.userRegistry.ready()),
this.onlineBoot.exit())
}
}
async _open() {
!1 !== (await this._ensureBuildType()) &&
(this._runBootOffline().catch(this.onerror),
this._runBootOnline().catch(this.onerror),
await this.offlineBoot.ready())
}
async _close() {
;(this.internalTime("keet-close"),
this.internalVerboseLog("Closing keet core"),
this._gcTimer && clearInterval(this._gcTimer),
this.backoff.destroy(),
this.internalTimeLog("keet-close", "destroyed backoff"),
this.state === ve.SUSPEND && (await this.store.resume()),
this.internalTimeLog("keet-close", "resumed from suspend"),
this.internalVerboseLog("Closing active calls"),
await this.backendAPI.closeActiveCalls(),
this.internalTimeLog("keet-close", "closed active calls"),
this.internalVerboseLog("Closing discovery cores"),
this.discoveryCore && (await this.discoveryCore.close()),
this.discoveryCoreSession && this.discoveryCoreSession.destroy(),
this.internalTimeLog("keet-close", "closed discovery cores"),
this.internalVerboseLog("Closing tasks"),
this.tasks && (await this.tasks.close()),
this.internalFastJoinSemaphore.destroy(),
this.internalTimeLog("keet-close", "stopped running tasks"))
for (const e of this._gcClosingRooms)
(this.internalTime(`gc-room-close-${e.id}`),
this.internalTimeLog(`gc-room-close-${e.id}`),
this.internalVerboseLog("Closing room in gc list", e.id, e.opened),
await e.close(),
this.internalTimeEnd(`gc-room-close-${e.id}`, "closed"))
;(this.internalTimeLog("keet-close", "closed rooms in gc"),
this.internalVerboseLog("Closing user registry"),
this.userRegistry && (await this.userRegistry.close()),
this.internalTimeLog("keet-close", "closed user registry"),
this.internalVerboseLog("Closing rpc client"),
this.rpcClient && (await this.rpcClient.close()),
this.internalTimeLog("keet-close", "closed rpc client"),
this.internalVerboseLog("Closing swarm worker"),
await this.swarmWorker.close(),
this.internalTimeLog("keet-close", "closed swarm worker"),
this.internalVerboseLog("Closing discovery"),
this.discovery && (await this.discovery.close()),
this.internalTimeLog("keet-close", "closed discovery"),
this.internalVerboseLog("Closing push"),
this.push && (await this.push.destroy()),
this.internalTimeLog("keet-close", "closed push"),
this.internalVerboseLog("Closing blind peering"),
this.blindPeering && (await this.blindPeering.close()),
this.internalTimeLog("keet-close", "closed blind peering"),
this.internalVerboseLog("Flushing DHT cache"),
this.swarm && (await this._flushDHTCache()),
this.internalTimeLog("keet-close", "flushed dht"),
this.internalVerboseLog("Closing swarm"),
this.swarm && (await this.swarm.destroy()),
this.internalTimeLog("keet-close", "destroyed swarm"),
this.internalVerboseLog("Closing files"),
await this.files.close(),
this.internalTimeLog("keet-close", "closed files"),
this.internalVerboseLog("Closing all pending rooms and workers"),
await Promise.allSettled([
...[...this.rooms.values()].map(async (e) => {
;(this.internalTime(`pending-room-close-${e.id}`),
this.internalTimeLog(`pending-room-close-${e.id}`),
await e.close(),
this.internalTimeEnd(`pending-room-close-${e.id}`))
}),
]),
this.internalTimeLog("keet-close", "closed all pending rooms and workers"),
this.internalVerboseLog("Closing backend"),
await this.apiV1.close(),
this.internalTimeLog("keet-close", "closed backend"),
await this.internalEnterLocalTransaction(),
await this.internalExitLocalTransaction(),
this.internalVerboseLog("Closing unread anchors"),
this.unreadsLoading && (await this.unreadsLoading),
this.unreads && (await this.unreads.close()),
this.internalTimeLog("keet-close", "closed unreads"),
this.internalVerboseLog("Closing wakeup"),
this.protomuxWakeup.destroy(),
this.internalTimeLog("keet-close", "protomux"),
this.internalVerboseLog("Closing hyperconf"),
this.hyperconf && (await this.hyperconf.close()),
this.internalVerboseLog("Closing error manager"),
this.errorManager && (await this.errorManager.close()),
this.internalVerboseLog("Closing local db"),
await this.local.close(),
this.internalVerboseLog("Closing contract bundles"),
this.contractBundles && (await this.contractBundles.close()),
this.internalVerboseLog("Closing store"),
await this.store.close(),
this.internalVerboseLog("Closing root corestore"),
await this.rootCorestore.close(),
this.internalTimeLog("keet-close", "closed cores"),
this.internalVerboseLog("Close done"),
this.internalTimeEnd("keet-close"))
}
async _ensureDebugState() {
const e = await this.internalEnterLocalTransaction()
try {
const t = (await e.get("@local/debug")) || {}
t.flags || (t.flags = X)
const i = new Set(),
s = {}
for (const e of t.flags) (i.add(e.key), (s[e.key] = e.value))
for (const { key: e, value: s, description: r } of X)
i.has(e) || t.flags.push({ key: e, value: s, description: r })
;(await e.insert("@local/debug", t), await e.flush(), (this.debug = { ...s }))
} finally {
await this.internalExitLocalTransaction()
}
}
isValidBuildType(e) {
if (!e) return !0
if (this.isTesting && !this.forceBuildType) return !0
const t = ye.Production === this.buildType
return !((!t && !e.settings.buildType) || (!t && e.settings.buildType !== this.buildType))
}
async _ensureBuildType() {
if (this.isTesting && !this.forceBuildType) return
const e = ye.Production === this.buildType,
t = await this.internalEnterLocalTransaction()
try {
const [i, s] = await Promise.all([t.get("@local/identity"), t.get("@local/environment")])
if (!e && i && !s?.buildType) throw ge.INVALID_BUILD_TYPE()
if (s && s.buildType !== this.buildType) throw ge.INVALID_BUILD_TYPE()
if (s) return
;(await t.insert("@local/environment", { buildType: this.buildType }), await t.flush())
} catch (e) {
return (this.onerror(e), !1)
} finally {
await this.internalExitLocalTransaction()
}
}
async _bumpLocalAbiVersion() {
const e = await this.internalEnterLocalTransaction()
try {
const t = await e.get("@local/environment")
;((t.latestOfflineAbi = this.abi.production),
await e.insert("@local/environment", t),
await e.flush())
} finally {
await this.internalExitLocalTransaction()
}
}
async _onDiscoveryConnect() {
const e = await this.local
.find("@local/rooms-by-timestamp", { reverse: !0, limit: 256 })
.toArray()
this.discovery.cork()
for (const t of e) {
const e = t.capabilities.discoveryKeyPair.publicKey,
i = await this.local.get("@local/background-sync", { discoveryPublicKey: e }),
s = Math.max(t.timestamps.latestSync, i ? i.lastSynced : 0)
this.discovery.subscribe(t.capabilities.discoveryKeyPair.publicKey, { since: s })
}
this.discovery.uncork()
}
async internalEnterLocalTransaction() {
return (await this._transactionLock.lock(), (this._tx = this.local.transaction()), this._tx)
}
async internalExitLocalTransaction() {
;(await this._tx.close(), this._transactionLock.unlock())
}
async _handleConfigUpdated() {
if (!this.isTesting || this.opts.hyperconfKey) {
if (
((this.config = this.hyperconf.current),
this.internalVerboseLog(
"OTA config updated, dkey:",
s.encode(this.hyperconf.core.discoveryKey),
"length:",
this.hyperconf.core.length,
),
(this._blindPeerKeys = this.config.services.blindPeers),
(this._relayKeys = this.config.services.blindRelays),
(this._gatekeeperKeys = this.config.services.gatekeepers),
(this._discoveryBootstrapKeys = this.config.services.discovery),
c.setRecoveryPeers && c.setRecoveryPeers(this.config.services.blindPeers),
(this._disabledRooms = new Set(this.config.rooms.disabled.map((e) => s.encode(e)))),
(this._verifiedRooms = new Set(this.config.rooms.verified?.map((e) => s.encode(e)))),
(this._disableUnreadSync = !0 === this.config.profile?.disableUnreadSync),
(this._otaResyncPrivateProfileBump = this.config.profile?.otaResyncPrivateProfileBump || 0),
this._maybeOtaResyncPrivateProfile().catch(this.onerror),
this._maybeUpgradeOpenRooms().catch(this.onerror),
this.config.services.userRegistries?.length &&
(this._registryOpts = this.config.services.userRegistries[0]),
this.blindPeering && this.blindPeering.setKeys(this._blindPeerKeys),
this.config.versions.minVersion)
) {
const { major: e, minor: t, patch: i } = this.config.versions.minVersion
this.configMinVersion = new be(e, t, i)
}
;(this.config.versions.autoupgrade > 0 &&
(this.abi.autoupgrade = this.config.versions.autoupgrade),
this.emit("force-update"),
this.emit("verified-rooms-changed"))
}
}
async _maybeUpgradeOpenRooms() {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const e = this.abi.autoupgrade,
t = this.config?.versions.autoupgrade
if (!(e < t)) return
const i = [],
s = async function (e, t) {
const [i, s] = await Promise.all([e.db.get("@keet/system-digest"), Y(e.db, e.deviceKey)])
!s?.status.isModerator ||
i?.abi.version >= t ||
(await this.internalDispatchUpgrade(e.id, t))
}.bind(this)
for (const [e, r] of this.rooms.entries()) i.push(s(r, t))
return await Promise.all(i)
}
async _maybeOtaResyncPrivateProfile() {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const e = this._otaResyncPrivateProfileBump || 0,
t = await this.internalEnterLocalTransaction()
try {
if (!(e > ((await t.get("@local/ota-actions"))?.otaResyncPrivateProfileBump || 0))) return
if (null === (await this.internalResyncPrivateProfile())) return
;(await t.insert("@local/ota-actions", { otaResyncPrivateProfileBump: e }), await t.flush())
} catch (e) {
this.onerror(e)
} finally {
await this.internalExitLocalTransaction()
}
}
shouldForceUpdate() {
const e = this.config.versions.forceUpdate > D.FORCE_UPDATE,
t =
this.configMinVersion && this.appVersion
? this.configMinVersion.compare(this.appVersion)
: 0
return e || 1 === t
}
isRoomVerified(e) {
return this._verifiedRooms.has(e)
}
async _bumpRoom(e) {
if (this.closing) return
if (!(await this.local.get("@local/writers-by-room-key", { roomKey: e }))) return
if (this.closing) return
const t = this.internalOpenExistingRoom(e)
try {
await t.ready()
} finally {
this.internalReleaseRoom(t)
}
}
_handleCoreOpen(e) {
const t = new c({ core: e, weak: !0 }),
i = new y({ debounce: !0 })
;(t.on("verification-error", (e) => {
this.onerror(e, { coreKey: t.key, prefix: "CORE_VERIFICATION_ERROR" })
}),
t.on("conflict", () => {
this.onerror(new Error("Conflict in Hypercore"), {
coreKey: t.key,
prefix: "CORE_CONFLICT",
})
}),
t.on("invalid-request", (e) => {
this.onerror(e, { coreKey: t.key, prefix: "CORE_INVALID_REQUEST" })
}),
t.on("append", async () => {
if (await i.lock())
try {
const t = e.state.storage.read(),
i = [t.getUserData("referrer"), t.getUserData("autobase/view")]
t.tryFlush()
const [s, r] = await Promise.all(i)
if (!s || r) return
await this._bumpRoom(s)
} catch (e) {
this.onerror(e)
} finally {
i.unlock()
}
}),
t.on("upload", (e, t) => {
;(this.stats.uploaded++, (this.stats.bytesUploaded += t))
}),
t.on("download", (e, t) => {
;(this.stats.downloaded++, (this.stats.bytesDownloaded += t))
}),
t.once("ready", () => {
if (t.writable) {
let e = t.length,
i = t.byteLength
t.on("append", () => {
const s = t.length,
r = t.byteLength
;((this.stats.appends += s - e), (this.stats.bytesAppended += r - i), (e = s), (i = r))
})
}
}))
}
async _handleWakeup(e) {
if (!this.discovery)
try {
const [t, i] = await Promise.all([
this.local.get("@local/writers-by-wakeup-topic", { wakeupTopic: e }),
this.local.get("@local/rooms-by-wakeup-topic", { capabilities: { wakeupTopic: e } }),
])
if (!t && !i) return
const s = t?.mailbox || i?.capabilities.mailboxKeyPair.publicKey,
r = this.protomuxWakeup.session(s, {
active: !1,
discoveryKey: e,
onpeeractive: (e) => {
t && r.announce(e, [{ key: t.key, length: 1 }])
},
})
i && (await this._bumpRoom(i.roomKey))
} catch (e) {
this.onerror(e)
}
}
async _maybeRunMigrations(e, t) {
;(3 === t && (await require("./lib/core/migrations/3")(this, e)),
4 === t && (await require("./lib/core/migrations/4")(this, e)),
5 === t && (await require("./lib/core/migrations/4")(this, e)),
t < 8 && (await require("./lib/core/migrations/7")(e)),
t < 9 && (await require("./lib/core/migrations/8")(e)))
}
async _maybeRunPostMigrations(e) {
;(4 === e && (await require("./lib/core/migrations/4/post")(this)),
5 === e && (await require("./lib/core/migrations/4/post")(this)))
}
async _ensureMigrated() {
let e = 0
const t = await this.internalEnterLocalTransaction()
try {
const i = await t.get("@local/migrations"),
s = i ? i.version : 3
;(s < 9 &&
((e = s),
await this._maybeRunMigrations(t, s),
await t.insert("@local/migrations", { version: 9 })),
await t.flush())
} finally {
await this.internalExitLocalTransaction()
}
const i = await this.local.find("@local/room-migrations-by-priority").toArray(),
r = [],
o = [],
a = []
for (const e of i)
this._disabledRooms.has(s.encode(e.roomKey))
? a.push(e)
: e.v3?.privateProfile
? r.push(e)
: o.push(e)
if (a.length > 0) {
const e = await this.internalEnterLocalTransaction()
try {
for (const t of a) {
const i = await e.get("@local/room-migrations", { roomKey: t.roomKey })
;(await e.delete("@local/room-migrations", t), await we(e, null, i))
}
await e.flush()
} finally {
await this.internalExitLocalTransaction()
}
}
await Promise.all(r.map((e) => this._migrateRoom(e, { migratingRooms: o })))
for (const e of o) this._migrateRoom(e).catch(this.onerror)
return e
}
async _migrateRoom(e, { migratingRooms: t } = {}) {
await this._roomMigrations.wait()
try {
const i = e.errored
if (
(i ||
(this.migrationStatus || (this.migrationStatus = { remaining: 0 }),
this.migrationStatus.remaining++,
this.emit("migration-status", this.migrationStatus)),
!e.timestamp)
) {
const t = await this.internalEnterLocalTransaction()
try {
e.timestamp = Date.now()
const i = await t.get("@local/room-migrations", e)
;(await t.insert("@local/room-migrations", e), await we(t, e, i), await t.flush())
} finally {
await this.internalExitLocalTransaction()
}
}
const s = Date.now() - e.timestamp > Se
let r = !1,
o = !1
try {
e.v3 &&
!s &&
(await require("./lib/core/migrations/3/room")(this, e.roomKey, e.v3, {
migratingRooms: t,
}))
} catch (e) {
;(this.onerror(e), (o = !0), (r = !!this.closing))
}
const a = await this.internalEnterLocalTransaction()
try {
const t = await a.get("@local/room-migrations", e)
;(o
? (i || (e.errored = !r), await a.insert("@local/room-migrations", e), await we(a, e, t))
: (await a.delete("@local/room-migrations", e), await we(a, null, t)),
await a.flush())
} finally {
await this.internalExitLocalTransaction()
}
i || (this.migrationStatus.remaining--, this.emit("migration-status", this.migrationStatus))
} finally {
this._roomMigrations.signal()
}
}
async _closeIdleRooms() {
const e = [],
t = []
for (const i of this.rooms.values())
i.refs > 0 ||
this.activeRooms.has(i) ||
++i.idleTicks < this._gcIdleTicks ||
this.backendAPI.callsByRoomId.get(i.id) ||
(this.rooms.delete(i.id), this._gcClosingRooms.add(i), e.push(i.close()), t.push(i))
await Promise.allSettled(e)
for (const e of t) this._gcClosingRooms.delete(e)
}
internalCloseRoom(e) {
;(this.rooms.delete(e.id), this._gcClosingRooms.add(e))
}
_debugJoinPeer(e) {
this.logger.info("Call join:", e)
}
_debugLeavePeer(e) {
this.logger.info("Call leave:", e)
}
debugPauseGC() {
this._gcTimer && (clearInterval(this._gcTimer), (this._gcTimer = null))
}
debugResumeGC() {
this._gcTimer ||
(this._gcTimer = setInterval(this._closeIdleRooms.bind(this), this._gcInterval))
}
async internalDiscoveryCompleted(e, t) {
try {
const e = this._discovering.get(t)
if (!e) return
const i = await this.internalEnterLocalTransaction()
try {
const t = e.roomInfo.capabilities.discoveryKeyPair.publicKey
e.room && this.internalReleaseRoom(e.room)
const s = await i.get("@local/background-sync", { discoveryPublicKey: t })
;(s &&
(await i.insert("@local/background-sync", { ...s, lastSynced: Date.now(), dirty: !1 })),
await i.flush())
} finally {
await this.internalExitLocalTransaction()
}
;(this._discovering.delete(t), this._discoverySemaphore.signal())
} catch (e) {
this.onerror(e)
}
}
async _startDiscoverySwarming(e, t) {
if (!(await this._discoverySemaphore.wait())) return !1
if (this.closing) return !1
const i = this._discovering.get(t)
return (
!!i &&
(this.swarmWorker.startDiscoverySwarming(e, { roomId: t }),
i.updated && this.swarmWorker.notifyDiscoveryUpdate(e),
!0)
)
}
async _refreshDiscoverySwarming() {
if ((await this._discoveryLock.lock()) && !this.closing)
try {
const e = await this.internalEnterLocalTransaction()
try {
for await (const { discoveryPublicKey: t } of e.find("@local/background-sync-by-dirty", {
reverse: !0,
})) {
const e = await this.local.get("@local/rooms-by-discovery", {
capabilities: { discoveryKeyPair: { publicKey: t } },
})
if (!e) continue
const i = s.encode(e.discoveryKey)
if (this._discovering.has(i)) continue
const r = this.internalOpenExistingRoom(e.roomKey)
;(this._discovering.set(i, { updated: !1, room: r, roomInfo: e }),
this._startDiscoverySwarming(e.capabilities.swarmTopic, i) ||
this.internalReleaseRoom(r))
}
await e.flush()
} finally {
await this.internalExitLocalTransaction()
}
} catch (e) {
this.onerror(e)
} finally {
this._discoveryLock.unlock()
}
}
async scheduleDiscoverySwarmingFromDiscoveryKey(e) {
const t = await this.local.get("@local/rooms-by-discovery-key", { discoveryKey: e })
if (t) return this.scheduleDiscoverySwarming(t.capabilities.discoveryKeyPair.publicKey)
}
async scheduleDiscoverySwarming(e) {
const t = await this.internalEnterLocalTransaction()
try {
const i = await t.get("@local/background-sync", { discoveryPublicKey: e }),
s = i?.lastSynced || 0
;(await t.insert("@local/background-sync", {
discoveryPublicKey: e,
timestamp: Date.now(),
lastSynced: s,
dirty: !0,
}),
await t.flush())
} finally {
await this.internalExitLocalTransaction()
}
this._refreshDiscoverySwarming()
}
async internalBlobsActiveInRoom(e) {
const t = s.encode(e),
i = this.rooms.get(t)
if (!i) return
const r = await this.local.get("@local/rooms", { roomKey: e })
if (!r) return
const { capabilities: o } = r
if (!o || !o.mailboxKeyPair) return
try {
await i.ready()
} catch {
return
}
if (!i.externalBlobs) return
const a = await this._getMirrorKeys(e, r)
i.closing ||
this.blindPeering.addCoreBackground(i.externalBlobs.core, {
target: o.mailboxKeyPair.publicKey,
keys: a,
pick: 1,
})
}
async _getMirrorKeys(e, t) {
if (this.debug.disableMirrors) return []
if (!e) return this._blindPeerKeys
const i = t || (await this.local.get("@local/rooms", { roomKey: e }))
return i && i.config.blindPeers?.length ? i.config.blindPeers : this._blindPeerKeys
}
_relayThrough(e) {
return e || this.swarm.dht.randomized ? this._relayKeys : null
}
internalVerboseLog(...e) {
if (this.isNightly || this.isTesting) return
const t = new Date()
this.logger.info(t, ...e)
}
internalTime(...e) {
this.isTesting || console.time(...e)
}
internalTimeLog(...e) {
this.isTesting || console.timeLog(...e)
}
internalTimeEnd(...e) {
this.isTesting || console.timeEnd(...e)
}
_handleConnection(e) {
if (this.closing) return void e.on("error", () => {})
const t = s.encode(e.publicKey),
i = s.encode(e.remotePublicKey),
r = Date.now()
let o = 0
this.internalVerboseLog(
`got ${e.isInitiator ? "client" : "server"} connection from ${t} to ${i}`,
)
let a = null
;(e.once("error", (e) => {
a = e
}),
e.once("close", () => {
this.internalVerboseLog(
`connection closed to ${i}, with err: ${a} (${Date.now() - r} ms ${o} bytes recv)`,
)
}),
e.on("data", (e) => {
o += e.byteLength
}),
this.protomuxWakeup.addStream(e),
this.rootCorestore.replicate(e))
}
async _createInitialIdentity() {
const t = e.generateMnemonic(),
i = this.tasks.restoreIdentity(Be, { mnemonic: t, recover: !1 })
await i.run()
}
async _loadIdentity() {
let e = await this.local.get("@local/identity")
if (
(e || (await this._createInitialIdentity()),
(e = await this.local.get("@local/identity")),
this.privateProfile)
)
return
const t = await this.local.get("@local/rooms", { roomKey: e.privateProfileKey })
;((this.privateProfile = await this.internalOpenPrivateProfile({ opening: !0 })),
this._shortenDeviceAttestations().catch(this.onerror),
this.swarmWorker.startActiveSwarming(t.capabilities.swarmTopic),
await this.internalRefreshUnreads())
}
async internalResyncPrivateProfile() {
if (!this.privateProfile) return null
const e = []
for await (const t of this.privateProfile.db.find("@keet-private-id/rooms"))
(await this.local.get("@local/rooms", { roomKey: t.roomKey })) || e.push(t)
for (const t of e) this.tasks.joinRoomKeys(t.roomKey, { keys: t }).runBackground()
}
async _shortenDeviceAttestations() {
const e = await this.local.get("@local/identity")
if (!e) return
const i = s.encode(e.privateProfileKey)
if (!(await this.internalFeatureSupported(i, I.SHORTEN_ATTESTATION))) return
const r = this.privateProfile.base.local.key,
o = f.decode(t, e.deviceProof).chain.length
for await (const i of this.privateProfile.db.find("@keet-private-id/device-identities")) {
const s = k.equals(i.deviceKey, r),
a = f.decode(t, i.deviceProof),
n = a.chain.length,
l = a.chain[n - 1].publicKey
if (!s && o + 1 < n) {
const t = He(e, i.deviceKey, l)
await this.privateProfile.dispatch("@keet-private-id/shorten-device-attestation", {
deviceKey: i.deviceKey,
deviceProof: t.deviceProof,
writerProof: t.privateProfileIdentity.proof,
})
}
if (s && f.decode(t, i.deviceProof).chain.length < o) {
const t = await this.internalEnterLocalTransaction()
try {
const s = { ...e, deviceProof: i.deviceProof, proofShortenedAt: Date.now() }
;(await t.insert("@local/identity", s), await t.flush())
} finally {
await this.internalExitLocalTransaction()
}
}
}
}
async internalDispatchUpgrade(e, t) {
if (!e || !t) return
const i = this.internalOpenExistingRoom(e)
try {
const e = this.batch(i.id)
if (0 === (await e.dispatch("@keet/upgrade", { version: t }))) return
if (t >= 14) {
const s = await i.db.get("@keet/system-digest"),
r = "@keet/migrate-abi",
o = {
header: { timestamp: e.timestamp },
from: s?.abi.version,
to: t,
buildType: this.buildType,
},
a = (await this.contractBundles.loadBundle(t)).encodeOperation(r, o)
await e.append(a)
}
await e.flush()
} finally {
this.internalReleaseRoom(i)
}
}
async _bumpCache() {
try {
if (!(await this._cacheLock.lock())) return
try {
await this._processCache()
} finally {
this._cacheLock.unlock()
}
} catch (e) {
this.onerror(e)
}
}
async _processCache() {
const e = []
for (; this._pendingTraces.length > 0; ) {
const t = this._pendingTraces.pop()
e.push(this._updateCache(t))
}
const t = await Promise.all(e),
i = await this.internalEnterLocalTransaction()
try {
for (const e of t) e && (await i.insert(e.collection, e.record))
await i.flush()
} finally {
await this.internalExitLocalTransaction()
}
}
async _updateCache({ roomKey: e, collection: t, record: i }) {
if ("@keet/members" === t) {
if (i?.profile.avatar) {
const t = this.internalOpenExistingRoom(e)
try {
await z(t.blobs, i.profile.avatar)
} finally {
this.internalReleaseRoom(t)
}
}
return { collection: "@local/members", record: i }
}
return "@keet/devices" === t
? { collection: "@local/devices", record: { roomKey: e, device: i } }
: null
}
_cacheRoomReads(e, t, i) {
;(this._pendingTraces.push({ roomKey: e, collection: t, record: i }), this._bumpCache())
}
_autobaseOpen(e, t) {
let i = null
t.internal || (i = (e, i) => this._cacheRoomReads(t.key, e, i))
const s = { autoUpdate: !0, extension: !1, trace: i },
r = x.autobaseOpen(e, t),
o = ee.bee(e, { hyperdb: s })
return { ...o, core: o.cores.db, legacy: r, roomKey: t.key }
}
_autobaseClose(e) {
return Promise.all([e.db.close(), e.blobs.close(), e.legacy.close()])
}
async _autobaseUpdate(e, t) {
const i = new U(this, e, t.base, t)
await i.run()
const r = s.encode(t.base.discoveryKey)
i.runPostUpdate()
const o = this._discovering.get(r)
o && ((o.updated = !0), this.swarmWorker.notifyDiscoveryUpdate(r))
}
async _autobaseApply(e, t, i, { dryRun: s = !1 } = {}) {
const { blobs: r, db: o, core: a, legacy: n, roomKey: l } = t,
c = s ? null : this._boundAutobaseTrace
let h = !1,
d = 0
if (!(a.length > 0)) {
if (((d = await N.autobaseApply(e, n, i)), d === e.length)) return !0
h = n.core.length > 0
}
const y = o.transaction(),
u = r.batch()
let p = null,
m = !1,
g = null
try {
if (h) {
const t = e[d]
if (s || t?.optimistic) return !0
;(i.preferFastForward(), await N.migrate(this.store, i.base, n, y, u, this.contractBundles))
}
const r = await y.get("@keet/system-digest")
if (r) {
const t = this.isNightly ? this.abi.max : this.abi.production
if (r.abi.version > t) {
if (s) return !1
i.interrupt({ unsupportedABI: r.abi.version, digest: r, nodes: e })
}
}
g = r ? r.abi.version : this.abi.autoupgrade
let o = await this.contractBundles.loadBundle(g),
a = await this.contractBundles.loadRouter(g)
y.setDefinition(o.spec.contract.hyperdb)
for (let r = d; r < e.length; r++) {
const n = e[r]
if (!(f.decode(f.uint, n.value) < 2))
try {
const r = se(n.value)
this._maybeLogAutobaseApply(r, s, e.length, g, l)
const h = Ge(y, u, n, i, t, a, { dryRun: s, trace: c, abi: this.abi })
;(await a.dispatch(r, h),
r &&
"@keet/upgrade" === r.name &&
((g = r.value.version),
(o = await this.contractBundles.loadBundle(g)),
(a = await this.contractBundles.loadRouter(g)),
y.setDefinition(o.spec.contract.hyperdb)))
} catch (e) {
if (te.isContractError(e)) throw e
if (re.isDispatchError(e)) throw te.DISPATCH_ERROR(e.message)
throw e
}
}
} catch (t) {
"BUNDLE_NOT_AVAILABLE" === t.code
? i.interrupt({ unsupportedABI: t.abi, digest: null, nodes: e })
: te.isContractError(t)
? ((m = !0), (t.abi = g), (t.roomKey = l), (s && !this.isTesting) || this.onerror(t))
: (p = t)
}
const b = !p && !m && y.updated()
if (
(s || p || m
? await Promise.all([u.close(), y.close()])
: await Promise.all([u.flush(), y.flush()]),
p)
)
throw p
return b
}
_maybeLogAutobaseApply(e, t, i, r, o) {
try {
const a = s.encode(o),
n = this.rooms.get(a),
l = n ? n.getQuickStats() : null
this.logger.verboseDebug("[autobaseApply]", {
"op.name": e.name,
"op.id": e.id,
dryRun: t,
"nodes.length": i,
abi: r,
roomId: a,
"roomStats.view": l?.view,
"roomStats.system": l?.system,
"roomStats.base": l?.base,
})
} catch (e) {
console.error("Failed to log autobase apply", e)
}
}
_autobaseTrace({ type: e, value: t }) {
if ("@keet/add-device" !== e) return
const { request: i, response: s, invitation: r } = t,
o = r?.metadata.expiration
if (o > 0 && o < Date.now()) return
const a = i.member.profile.mailbox,
n = i.keyPair
a && n && this._processAutobaseTrace(a, n, s).catch(this.onerror)
}
async _processAutobaseTrace(e, t, i) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const s = await this.local.get("@local/identity"),
r = await this.local.get("@local/rooms", { roomKey: s.privateProfileKey })
k.equals(e, r.capabilities.mailboxKeyPair.publicKey) ||
this.tasks
.joinRoomResponse(t.publicKey, { mailbox: e, keyPair: t, response: i })
.runBackground()
}
async _backgroundDrain(e) {
let t = await e.next()
try {
for (; !t.done && !this.closing; ) {
const i = await t.value
if (this.closing) return
t = await e.next(i)
}
} finally {
!t.done && e.return && (await e.return())
}
}
async _safeLoadLegacyUnreads() {
try {
const e = []
for await (const t of this.local.find("@local/rooms")) {
const i = await this.local.get("@local/unreads", { roomKey: t.roomKey })
i && e.push(i)
}
return e
} catch {
return []
}
}
async *_migrateUnreadAnchors() {
const e = yield this._safeLoadLegacyUnreads(),
t = yield this.internalEnterLocalTransaction()
try {
const i = this.unreads.write(),
s = Date.now()
for (const { roomKey: r, count: o } of e)
(i.tryPut(r, { length: o + 1, timestamp: s }),
yield t.delete("@local/unreads", { roomKey: r }))
;(yield i.flush(), yield t.flush())
} finally {
yield this.internalExitLocalTransaction()
}
}
async *_internalRefreshUnreads() {
if (!this.privateProfile) return
let e = yield this.privateProfile.db.get("@keet/system-digest")
if (
e &&
(yield this.privateProfile.internalEnsureUpgraded({ opening: !0 }),
(e = yield this.privateProfile.db.get("@keet/system-digest")),
D.isFeatureSupported(e.abi.version, D.FEATURES.UNREADS) && (yield this._unreadsLock.lock()))
)
try {
const t = []
let i = !0
const s = D.isFeatureSupported(e.abi.version, D.FEATURES.V2_UNREADS)
? "@keet-private-id/unreads-v2"
: "@keet-private-id/unreads",
r = yield this.privateProfile.db.find(s).toArray()
for (const { deviceKey: e, key: s } of r)
(k.equals(e, this.privateProfile.base.local.key) && (i = !1), t.push(s))
if (this.unreads && Xe(t, this.unreads.keys)) return
this.unreads && (yield this.unreads.close(), (this.unreads = null))
const o = this.privateProfile,
a = o.base.encryptionKey,
n = this.store.namespace(o.key),
l = n.namespace(_e)
yield n.close()
const c = yield o.base.wakeupCapability && o.base.wakeupCapability.key
;((this.unreads = new E(l, t, {
blindPeering: this.blindPeering,
encryptionKey: a,
target: c,
})),
this.unreads.on("remote-update", () => this.emit("unread-anchors-changed")),
yield this.unreads.ready(),
i &&
(yield this._backgroundDrain(this._migrateUnreadAnchors()),
yield this.privateProfile.dispatch("@keet-private-id/add-unreads-key", {
key: this.unreads.local.key,
})))
} finally {
this._unreadsLock.unlock()
}
}
internalRefreshUnreads() {
return this._disableUnreadSync
? Promise.resolve()
: this._backgroundDrain(this._internalRefreshUnreads())
}
async internalLoadDiscoveryCore() {
try {
const e = await this.privateProfile.db.get("@keet-private-id/identity")
if (!e || !e.discoveryCoreKey || this.closing) return
if (this.discoveryCore) return
;((this.discoveryCore = this.store.get({ key: e.discoveryCoreKey })),
await this.discoveryCore.ready(),
this.blindPeering.addCoreBackground(this.discoveryCore, { pick: 3 }))
} catch (e) {
this.onerror(e)
}
}
async internalCreateDiscoveryCore(e) {
const t = e.getProfileDiscoveryEncryptionKey(),
i = { version: 2, signers: [{ publicKey: e.profileDiscoveryKeyPair.publicKey }] }
return (
this.discoveryCore && (await this.discoveryCore.close()),
(this.discoveryCore = this.store.get({
keyPair: e.profileDiscoveryKeyPair,
valueEncoding: de,
encryptionKey: t,
manifest: i,
})),
await this.discoveryCore.ready(),
this.blindPeering.addCoreBackground(this.discoveryCore, { pick: 3 }),
(this.discoveryCoreSession = this.swarm.join(this.discoveryCore.discoveryKey, {
server: !1,
client: !0,
})),
!0
)
}
async internalDryRun(e, t, i) {
const s = i.map((t, i) => ({
length: e.base.local.length + 1 + i,
from: e.base.local,
value: t,
heads: [],
}))
return !!(await this._autobaseApply(s, t, e.base, { dryRun: !0 }))
}
internalGetHeads(e, t) {
const i = []
let s = !1
if (t)
for (const r of t.base.heads())
e && k.equals(r.key, e.key)
? ((s = !0), i.push({ key: r.key, length: Math.max(r.length, e.length) }))
: i.push(r)
return (e && !s && i.push(e), i)
}
async internalGetKeyPair(e) {
const t = await this.local.get("@local/writers-by-room-key", { roomKey: e })
return t ? t.keyPair : null
}
async internalGetMailbox(e) {
const t = await this.local.get("@local/rooms", { roomKey: e })
return t
? { key: t.capabilities.mailboxKeyPair.publicKey, discoveryKey: t.capabilities.wakeupTopic }
: null
}
async internalGetEncryptionKey(e) {
const t = await this.local.get("@local/rooms", { roomKey: e })
return !t || t.status.isRemoved ? null : t.encryptionKey
}
internalReleaseRoom(e) {
if (0 === e.refs) throw new Error("Cannot release a room that was not previously opened")
e.refs -= 1
}
internalOpenExistingRoom(e, t = {}) {
return this.internalOpenRoom(s.decode(e), null, t)
}
async internalOpenPrivateProfile({ opening: e = !1 } = {}) {
e || this.onlineBoot.opened || (await this.onlineBoot.ready())
const t = await this.local.get("@local/identity"),
i = this.internalOpenExistingRoom(t.privateProfileKey, { bigBatches: !0 })
return (
await i.ready(),
(this.privateProfile && k.equals(i.key, this.privateProfile.key)) ||
(this.privateProfile && this.internalReleaseRoom(this.privateProfile),
(this.privateProfile = this.internalOpenExistingRoom(t.privateProfileKey, {
bigBatches: !0,
})),
(this.unreadsLoading = this.internalRefreshUnreads().catch(() => null))),
this.internalLoadDiscoveryCore(),
i
)
}
async internalSafeSwapPrivateProfiles() {
try {
const e = await this.internalOpenPrivateProfile()
this.internalReleaseRoom(e)
} catch (e) {
this.onerror(e)
}
}
async internalFeatureSupported(e, t) {
const i = s.decode(e),
r = await this.local.get("@local/rooms", { roomKey: i })
return !!r && this.versions.isFeatureSupported(r.digest.abi.version, t)
}
internalOpenRoom(e, t, { fastForwardTo: i, mailbox: r, keyPair: o, bigBatches: a } = {}) {
if (this.closing) throw new Error("Keet is closing")
const n = s.encode(e),
l = this.rooms.get(n)
if (l) return ((l.idleTicks = 0), (l.refs += 1), l)
const c = w.discoveryKey(e),
h = this.store.namespace(c)
let y = null,
u = null,
p = null
;(r || ((u = this.internalGetMailbox(e)), u.catch(this.onerror)),
o || ((y = this.internalGetKeyPair(e)), y.catch(this.onerror)),
t || ((p = this.internalGetEncryptionKey(e)), p.catch(this.onerror)),
r && (r = { key: r, discoveryKey: w.discoveryKey(r) }))
const m = new d(h, e, {
backoff: this.backoff,
update: this._autobaseUpdate.bind(this),
apply: this._autobaseApply.bind(this),
close: this._autobaseClose.bind(this),
open: this._autobaseOpen.bind(this),
encryptionKey: t || p,
wakeupCapability: r || u,
keyPair: o || y,
fastForward: !this.debug.disableFastForward && i,
ackThreshold: this.ackThreshold,
ackInterval: this.ackInterval,
wakeup: this.protomuxWakeup,
optimistic: !0,
encrypted: !0,
bigBatches: !!a,
})
;(m.on("interrupt", (e) => {
;(this.logger.warn("room=", m.id, "got interrupted"), this.emit("room-interrupted", n, e))
}),
m.on("warning", (t) => {
;(this.logger.warn("[autobase warning] room=", m.id, "error=", t),
this.onerror(t, { roomKey: e, prefix: "AUTOBASE_WARNING" }))
}),
m.on("error", (t) => {
;(this.logger.warn("[autobase error] room=", m.id, "error=", t),
this.onerror(t, { roomKey: e, prefix: "AUTOBASE_ERROR" }))
}))
const g = new je(this, n, e, c, m)
return (this.rooms.set(n, g), (g.refs += 1), g)
}
async updateDevice({ name: e } = {}) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const t = { swarmKey: this.swarm.keyPair.publicKey, name: e, isMobile: this.isMobile },
i = await this.internalOpenPrivateProfile()
try {
await i.dispatch("@keet/update-device", { profile: t })
} finally {
this.internalReleaseRoom(i)
}
}
async internalUpdateProfile(e, { forceUpdateRegistry: t = !1, opening: i = !1 } = {}) {
const s = await this.internalGetIdentityInfo({ opening: i })
if (this.closing) return
if (this._rejectAnonymousProfiles && !e.displayName && !t)
throw new Error("A member must have a profile name")
const r = this.internalCreateMemberUpdate(s, { profile: e }),
o = await this.internalOpenPrivateProfile({ opening: i })
if (this.closing) return
let a = null
try {
a = await o.dispatch("@keet/update-member", { member: r })
} finally {
this.internalReleaseRoom(o)
}
;(a || t) && r.profile.username && this.userRegistry?.internalUpdateRegistryMember(r)
}
async updateProfile(e = {}) {
return (
this.onlineBoot.opened || (await this.onlineBoot.ready()),
this.internalUpdateProfile(e)
)
}
async internalVerifyOptimisticCore(e) {
return !(await e.getUserData("referrer"))
}
async internalAppendOptimisticOperation(e, t) {
const i = this.store.get({ key: e, exclusive: !0 })
;(await i.ready(),
(await this.internalVerifyOptimisticCore(i))
? (await i.append(t), await i.close())
: await i.close())
}
async internalCreateRoom(e = {}) {
const t = e.seed || w.randomBytes(32),
i = e.keyPair || w.keyPair(),
s = e.encryptionKey || w.randomBytes(32),
r = W(i.publicKey),
o = w.discoveryKey(r),
a = G(r, t),
n = a.mailboxKeyPair.publicKey,
l = await this.internalEnterLocalTransaction()
try {
;(await l.insert("@local/writers", { mailbox: n, keyPair: i, key: r, roomKey: r }),
await l.flush())
} finally {
await this.internalExitLocalTransaction()
}
const c = {
header: { timestamp: Date.now() },
digest: {
abi: { version: this.isExperimental ? this.abi.production : this.abi.autoupgrade },
creationTime: Date.now(),
settings: {
roomType: 1,
...e.settings,
isExperimental: this.isExperimental,
buildType: this.buildType,
},
},
capabilities: { seed: t },
config: e.config,
identity: e.identity,
initialMembers: e.initialMembers,
bootstrapDevice: e.bootstrapDevice,
},
h = this.tasks.createRoom(o, {
roomKey: r,
encryptionKey: s,
mailbox: n,
keyPair: i,
genesis: c,
isPrivateProfile: !!e.isPrivateProfile,
})
return (
await h.run(),
this.discovery &&
this.discovery.subscribe(a.discoveryKeyPair.publicKey, { since: Date.now() }),
this.swarmWorker.refresh().catch(ze),
this.internalOpenExistingRoom(r)
)
}
async createRoom(e) {
this.opened || (await this.ready())
const t = await this.internalCreateRoom(e)
return (this.internalReleaseRoom(t), t.key)
}
async leaveRoom(e) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const t = await this.local.get("@local/identity"),
i = this.internalOpenExistingRoom(e)
await i.ready()
try {
await i.dispatch("@keet/remove-member", { key: t.publicKey })
} catch (e) {
this.onerror(e)
} finally {
this.internalReleaseRoom(i)
}
await this.privateProfile.dispatch("@keet-private-id/update-room-status", {
roomKey: e,
key: Q.IsActive,
status: { isActive: !1 },
})
}
async activateRoom(e) {
if (this.closing) return
const t = await this.local.get("@local/rooms", { roomKey: e })
if (!t || this.closing) return
const i = this.internalOpenExistingRoom(e)
;(await i.ready(),
this.activeRooms.has(i)
? this.internalReleaseRoom(i)
: (this.activeRooms.add(i),
this.swarmWorker.startActiveSwarming(t.capabilities.swarmTopic)))
}
async deactivateRoom(e) {
const t = this.internalOpenExistingRoom(e)
if (!this.activeRooms.has(t)) return void this.internalReleaseRoom(t)
const i = await this.local.get("@local/rooms", { roomKey: e })
;(this.activeRooms.delete(t),
this.swarmWorker.stopActiveSwarming(i.capabilities.swarmTopic),
this.internalReleaseRoom(t),
this.internalReleaseRoom(t))
}
async internalGetIdentityInfo({ opening: e = !1 } = {}) {
e || this.onlineBoot.opened || (await this.onlineBoot.ready())
const t = await this.local.get("@local/identity")
if (!t) throw new Error("Identity does not exist")
const i = await this.local.get("@local/rooms", { roomKey: t.privateProfileKey })
if (!i) throw new Error("Private profile does not exist")
return { identity: t, privateProfile: i }
}
internalGetMemberUpdate(e, { profile: t } = {}) {
const i = e.privateProfile
return i?.selfMember
? i.selfMember.proofs
? {
profile: i.selfMember.profile,
attestation: {
memberKey: i.selfMember.key,
proof: i.selfMember.proofs.profileAttestation,
},
}
: this.internalCreateMemberUpdate(e)
: null
}
internalAttestData(t, i) {
return e.attestData(t, i.deviceKeyPair, i.deviceProof)
}
internalCreateDeviceUpdate(e, t, { isPrivateProfile: i = !1, buildType: s = null } = {}) {
const r = e.identity
let o = null
e.privateProfile?.selfDevice && i && (o = e.privateProfile.selfDevice.profile.name)
const a = e.privateProfile?.digest.settings.buildType,
n = ye.Production === a
return {
profile: {
swarmKey: this.swarm.keyPair.publicKey,
name: o,
isMobile: this.isMobile,
buildType: n && null === s ? null : a,
},
attestation: { memberKey: r.publicKey, proof: this.internalAttestData(t, r) },
}
}
internalCreateMemberUpdate(t, { profile: i } = {}) {
const s = t.privateProfile?.selfMember?.profile || null,
r = t.privateProfile?.capabilities.mailboxKeyPair.publicKey || i.mailbox,
o = t.identity,
a = { ...s, ...i, mailbox: r, timestamp: Date.now() },
n = ne("@keet/member-profile", a)
return {
profile: a,
attestation: {
memberKey: o.publicKey,
proof: e.attestData(n, o.deviceKeyPair, o.deviceProof),
},
}
}
internalGetLinkInfo(e) {
try {
return this.internalDecodeInvitation(e)
} catch {}
try {
return this.internalDecodeContactInfo(e)
} catch {}
return null
}
internalDecodeInvitation(e) {
try {
const t = "string" == typeof e ? v.decode(e) : e
return le("@keet/sendable-invitation", t)
} catch {
throw ge.INVALID_INVITE(`Invalid invite: ${e}`)
}
}
internalDecodeContactInfo(e) {
try {
const t = k.isBuffer(e) ? e : k.from(e, "base64")
return le("@keet/member-contact-info", t)
} catch {
throw ge.INVALID_CONTACT_INFO(`Invalid contact info: ${e}`)
}
}
internalEncodeContactInfo(e) {
return ne("@keet/member-contact-info", e)
}
internalCreateContactInfoStruct(t, i, s = {}) {
const r = { ...i.selfMember.profile, ...s },
o = ne("@keet/member-profile", r),
a = {
profile: r,
attestation: {
memberKey: i.selfMember.key,
proof: e.attestData(o, t.deviceKeyPair, t.deviceProof),
},
}
return this.internalEncodeContactInfo(a)
}
async wakeup() {
;((this.state = ve.WAKEUP),
this.internalVerboseLog("[Keet Core]", "await .ready"),
this.opened || (await this.ready()))
const e = [this.resume(), this.swarmWorker.background()]
;(this.internalVerboseLog("[Keet Core]", "await all promise"),
await Promise.allSettled(e),
this.internalVerboseLog("[Keet Core]", "promise settled"))
}
async presuspend() {
;((this.state = ve.PRESUSPEND),
this.opened || (await this.ready()),
this.swarmWorker.setServerMode(!1))
}
async suspend() {
if ((this.internalVerboseLog("[Keet Core]", "suspend called"), this.state === ve.SUSPEND))
return
;((this.state = ve.SUSPEND),
this.opened || (await this.ready()),
this.internalVerboseLog("[Keet Core]", "suspend post ready"))
const e = [this._flushDHTCache()]
;(this.blindPeering &&
(this.internalVerboseLog("[Keet Core]", "suspend adding blind peering"),
e.push(this.blindPeering.suspend())),
this.push &&
(this.internalVerboseLog("[Keet Core]", "suspend push"), e.push(this.push.suspend())),
this.discovery &&
(this.internalVerboseLog("[Keet Core]", "suspend discovery"),
e.push(this.discovery.suspend())),
await Promise.allSettled(e),
this.internalVerboseLog("[Keet Core]", "all done"),
await this.rpcClient.suspend(),
this.internalVerboseLog("[Keet Core]", "rpc suspended"),
await this.swarm.suspend(),
this.internalVerboseLog("[Keet Core]", "swarm suspend"),
await this.files.server.suspend(),
this.internalVerboseLog("[Keet Core]", "files suspend"),
await this.store.suspend(),
this.internalVerboseLog("[Keet Core]", "store suspended, all done"))
}
async resume() {
if ((this.internalVerboseLog("[Keet Core]", "resume called"), this.state === ve.RESUME)) return
;((this.state = ve.RESUME),
this.internalVerboseLog("[Keet Core]", "await this.ready"),
this.opened || (await this.ready()),
this.internalVerboseLog("[Keet Core]", "resume store"),
await this.store.resume(),
this.internalVerboseLog("[Keet Core]", "resume file server"),
await this.files.server.resume(),
this.internalVerboseLog("[Keet Core]", "resume swarm"),
this.readOnly || (await this.swarm.resume()),
this.internalVerboseLog("[Keet Core]", "resume rpc client"),
await this.rpcClient.resume())
const e = []
;(this.blindPeering && !this.readOnly && e.push(this.blindPeering.resume()),
this.push && e.push(this.push.resume()),
this.discovery && e.push(this.discovery.resume()),
this.swarmWorker.setServerMode(!0),
e.push(this.swarmWorker.unbackground()),
this.internalVerboseLog("[Keet Core]", "await all promises"),
await Promise.allSettled(e),
this.internalVerboseLog("[Keet Core]", "all promises settled"))
}
async _createInvitation(e, t) {
let {
expiration: i = 0,
canAdmin: s = !1,
canModerate: r = !1,
identityPublicKey: o = null,
requiresConfirmation: a = !1,
readOnly: n = !1,
} = t
if ("string" == typeof i)
if ("2h" === i) i = Date.now() + 72e5
else if ("2d" === i) i = Date.now() + 1728e5
else {
if ("2w" !== i) throw ge.UNKNOWN_EXPIRATION(`Unknown expiration: ${i}`, { roomKey: e.key })
i = Date.now() + 12096e5
}
await e.ready()
const [{ seed: l }, c, h] = await Promise.all([
e.db.get("@keet/room-capabilities"),
Y(e.db, e.deviceKey),
e.db.get("@keet/system-digest"),
])
if ((r || s) && !c.status.isModerator)
throw ge.MODERATORS_ONLY("Only moderators can create moderator invitations", {
roomKey: e.key,
})
const d = G(e.key, l),
y = this.swarm.dht.toArray({ limit: 3 })
if (n)
return ne("@keet/sendable-invitation", {
readonly: {
roomKey: e.key,
encryptionKey: e.encryptionKey,
fastForwardTo: e.base.getSystemKey(),
seed: l,
},
additionalNodes: y,
})
const u = { key: e.key, encryptionKey: e.encryptionKey, fastForwardTo: e.base.getSystemKey() },
{ invitation: p, seed: m } = $(u, {
requiresConfirmation: a,
expiration: i,
canModerate: r,
canAdmin: s,
})
await e.dispatch("@keet/add-invitation", { invitation: p })
const g = (await e.db.get("@keet/invitations", { id: p.id })).anchor,
b = d.mailboxKeyPair.publicKey,
w = {
identity: null,
room: null,
contact: null,
additionalNodes: y,
buildType: h.settings.buildType,
}
return (
o
? (w.identity = { mailbox: b, invitationSeed: m, anchor: g, identityPublicKey: o })
: (w.room = { mailbox: b, invitationSeed: m, anchor: g }),
ne("@keet/sendable-invitation", w)
)
}
async createRoomInvitation(e, t = {}) {
this.opened || (await this.ready())
const i = s.decode(e),
r = this.internalOpenExistingRoom(i)
try {
const e = await this._createInvitation(r, t)
return v.encode(e)
} finally {
this.internalReleaseRoom(r)
}
}
async createIdentityInvitation() {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const e = await this.local.get("@local/identity"),
t = this.internalOpenExistingRoom(e.privateProfileKey)
this.blindPeering.addAutobaseBackground(t.base)
try {
const i = await this._createInvitation(t, {
expiration: "2h",
canAdmin: !0,
canModerate: !0,
requiresConfirmation: !0,
identityPublicKey: e.publicKey,
})
return v.encode(i)
} finally {
this.internalReleaseRoom(t)
}
}
async _startPairing(e, { deviceKeyPair: t, buildType: i = null } = {}) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const r = j(e.invitationSeed),
o = e.invitationSeed,
a = e.mailbox,
n = e.anchor,
l = { key: null, seedId: s.encode(o) },
c = await this.local.get("@local/rooms-by-mailbox", {
capabilities: { mailboxKeyPair: { publicKey: a } },
})
let h = null
if (c) {
if (c.selfMember?.status.isRemoved) throw new Error("Removed member cannot rejoin")
h = await this.local.get("@local/room-preferences", { roomKey: c.roomKey })
}
if (h && h.status.isActive && !h.status.isReadOnly) return ((l.key = s.encode(c.roomKey)), l)
const d = w.keyPair(),
y = await this.store.createKeyPair(J(d.publicKey)),
u = this.store.get({ keyPair: y, active: !1 })
await u.ready()
const p = { key: u.key, length: 1 }
await u.close()
let m = null
t && (m = { writerKey: u.key, publicKey: e.identityPublicKey, devicePublicKey: t.publicKey })
const g = {
id: r,
mailbox: a,
inviter: n,
identity: m,
initiator: p,
responseKeyPair: d,
invitationSeed: o,
buildType: i,
},
b = await this.privateProfile.db.find("@keet-private-id/pending-invitations").toArray()
for (const e of b)
k.equals(e.mailbox, g.mailbox) &&
(await this.privateProfile.dispatch("@keet-private-id/remove-pending-invitation", e))
return (
await this.privateProfile.dispatch("@keet-private-id/add-pending-invitation", {
invitation: g,
}),
l
)
}
async _openReadOnlyRoom({ roomKey: e, encryptionKey: t, seed: i, fastForwardTo: r }) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const { mailboxKeyPair: o, swarmTopic: a } = G(e, i),
n = this.internalOpenRoom(e, t, { mailbox: o.publicKey, fastForwardTo: { key: r } })
return (
await n.ready(),
await this.privateProfile.dispatch("@keet-private-id/add-room", {
roomKey: e,
encryptionKey: t,
mailbox: o.publicKey,
fastForwardTo: r,
isReadOnly: !0,
}),
this.swarmWorker.startDiscoverySwarming(a, { roomId: s.encode(e) }),
{ key: s.encode(e), seedId: s.encode(i) }
)
}
async startPairingRoom(e, t) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const i = this.internalDecodeInvitation(e),
s = await this.local.get("@local/rooms", { roomKey: this.privateProfile.key }),
r = i.buildType,
o = s.digest.settings.buildType || ye.Production
if (r && r !== o) throw ge.INVALID_BUILD_TYPE()
if (i.readonly) return this._openReadOnlyRoom(i.readonly)
if (!i.room) throw ge.INVALID_INVITATION_TYPE("Invalid room invitation")
return this._startPairing(i.room, { ...t, buildType: r })
}
async startPairingIdentity(e) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const t = this.internalDecodeInvitation(e),
i = await this.local.get("@local/rooms", { roomKey: this.privateProfile.key }),
s = t.buildType,
r = i.digest.settings.buildType || ye.Production
if (s && s !== r) throw ge.INVALID_BUILD_TYPE()
if (!t.identity) throw ge.INVALID_INVITATION_TYPE("Invalid device sync invitation")
const o = H(t.identity.identityPublicKey),
a = await this.store.createKeyPair(o)
return this._startPairing(t.identity, { deviceKeyPair: a, buildType: s })
}
async stopPairing(e) {
this.opened || (await this.ready())
const t = await this.internalOpenPrivateProfile()
try {
if (!(await t.db.get("@keet-private-id/pending-invitations", { id: e }))) return
await t.dispatch("@keet-private-id/remove-pending-invitation", { id: e })
} finally {
this.internalReleaseRoom(t)
}
}
async rotateWriter(e) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const [t, i, s] = await Promise.all([
this.local.get("@local/writers-by-room-key", { roomKey: e }),
this.local.get("@local/rooms", { roomKey: e }),
this.privateProfile.db.get("@keet-private-id/rooms", { roomKey: e }),
])
if (!i || !t || !s) return
const r = await this.internalEnterLocalTransaction()
try {
;(await r.delete("@local/writers", t), await r.flush())
} finally {
await this.internalExitLocalTransaction()
}
this.tasks.joinRoomKeys(s.mailbox, { keys: s }).runBackground()
}
async getRecoveryPhrase() {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const e = (await this.local.get("@local/identity")).entropy
return e ? C({ entropy: e }) : null
}
async getEncryptedRecoveryPhrase(e) {
if ("string" != typeof e) throw new Error("Password required")
if (0 === e.length) throw new Error("Password should not be empty")
const t = await this.getRecoveryPhrase()
if ("string" != typeof t) throw new Error("Recovery phrase reading failed")
if (0 === t.length) throw new Error("Recovery phrase reading failed")
return O.encryptPhrase(t, e)
}
decryptEncryptedRecoveryPhrase(e, t) {
if (!ArrayBuffer.isView(e)) throw new Error("Encrypted phrase should be an ArrayBuffer view")
if (0 === e.byteLength) throw new Error("Encrypted phrase should not be empty")
if ("string" != typeof t) throw new Error("Password required")
if (0 === t.length) throw new Error("Password should not be empty")
return O.decryptPhrase(e, t)
}
freeRecoveryPhrase(e) {
O.free(e)
}
async storeRecoveryPhrase(t) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const i = await e.from({ mnemonic: t }),
s = await this.local.get("@local/identity"),
r = await this.local.get("@local/rooms", { roomKey: s.privateProfileKey })
if (!k.equals(s.publicKey, i.identityPublicKey)) throw new Error("Wrong recovery phrase")
if (!r.identity.discoveryCoreKey) {
if ((await this.internalCreateDiscoveryCore(i), this.discoveryCore.length > 0)) {
const e = Math.floor((Date.now() - qe) / 1e3)
await this.discoveryCore.truncate(this.discoveryCore.length, e)
}
;(await this.discoveryCore.append({
version: 1,
timestamp: Date.now(),
key: r.roomKey,
mailbox: r.capabilities.mailboxKeyPair.publicKey,
}),
await this.privateProfile.dispatch("@keet-private-id/update-identity", {
identity: { publicKey: i.identityPublicKey, discoveryCoreKey: this.discoveryCore.key },
}))
}
}
async confirmRecoveryPhrase({ persist: e = !1 } = {}) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const t = await this.local.get("@local/identity")
;(e || (t.entropy = null), (t.status.isConfirmed = !0))
const i = await this.internalEnterLocalTransaction()
try {
;(await i.insert("@local/identity", t), await i.flush())
} finally {
await this.internalExitLocalTransaction()
}
this.emit("identity-changed")
}
async restoreIdentity(t, { create: i = !0 } = {}) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const [s] = await Promise.all([this.local.get("@local/identity"), e.from({ mnemonic: t })]),
r = w.discoveryKey(s.privateProfileKey),
o = this.tasks.restoreIdentity(r, {
mnemonic: t,
recover: !1 === i,
profileDiscoveryTimeout: this._profileDiscoveryTimeout,
buildType: this.buildType,
})
await o.run()
const a = await this.local.get("@local/identity"),
n = await this.local.get("@local/rooms", { roomKey: a.privateProfileKey })
;((this.privateProfile = await this.internalOpenPrivateProfile()),
this.swarmWorker.startActiveSwarming(n.capabilities.swarmTopic))
}
async acceptJoinRequest(e, t) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const i = this.internalOpenExistingRoom(e)
try {
const s = await i.db.get("@keet/join-requests", { deviceKey: t })
if (!s) return
const r = s.request
let o = null
if (r.identity) {
const i = await this.local.get("@local/identity")
if (!k.equals(i.privateProfileKey, e))
throw new Error(
"Accepting an identity device join request on a room that is not your private profile",
)
o = He(i, t, r.identity.devicePublicKey)
}
await i.dispatch("@keet/accept-join-request", { deviceKey: t, identity: o })
} finally {
this.internalReleaseRoom(i)
}
}
async rejectJoinRequest(e, t) {
this.opened || (await this.ready())
const i = this.internalOpenExistingRoom(e)
try {
if (!(await i.db.get("@keet/join-requests", { deviceKey: t })))
throw new Error("Join request for that device key does not exist")
await i.dispatch("@keet/reject-join-request", { deviceKey: t })
} finally {
this.internalReleaseRoom(i)
}
}
async _sendContactRequest(e) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const t = await this.internalOpenPrivateProfile()
try {
this.tasks.sendContactRequest(e.memberKey, e).runBackground()
} finally {
this.internalReleaseRoom(t)
}
}
async sendContactRequestFromContact(e, { message: t }) {
const i = (e = this.internalDecodeContactInfo(e)).attestation.memberKey,
s = e.attestation.memberKey
return this._sendContactRequest({ roomKey: i, memberKey: s, message: t, contactInfo: e })
}
async sendContactRequest(e, t, { message: i }) {
return this._sendContactRequest({ roomKey: e, memberKey: t, message: i })
}
async blockContact(e) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const [t, i, s] = await Promise.all([
this.privateProfile.db.findOne("@keet-private-id/contact-requests-by-member", {
gte: { memberKey: e },
lte: { memberKey: e },
}),
this.local.get("@local/contacts", { memberKey: e }),
this.local.get("@local/members", { key: e }),
]),
r = i?.mailbox || s?.profile.mailbox || t?.sender?.mailbox
if (!r) throw new Error("Cannot find contact")
await this.privateProfile.dispatch("@keet-private-id/add-contact", {
contact: { memberKey: e, mailbox: r, status: { isBlocked: !0 } },
})
}
async unblockContact(e) {
this.onlineBoot.opened || (await this.onlineBoot.ready())
const [t, i, s] = await Promise.all([
this.privateProfile.db.findOne("@keet-private-id/contact-requests-by-member", {
gte: { memberKey: e },
lte: { memberKey: e },
}),
this.local.get("@local/contacts", { memberKey: e }),
this.local.get("@local/members", { key: e }),
]),
r = i?.mailbox || s?.profile.mailbox || t?.sender?.mailbox
if (!r) throw new Error("Cannot find contact")
await this.privateProfile.dispatch("@keet-private-id/add-contact", {
contact: { memberKey: e, mailbox: r, status: { isBlocked: !1 } },
})
}
async _flushDHTCache() {
if (this.readOnly) return
const e = this.swarm.dht.toArray({ limit: Me })
try {
await this._setCachedDHTNodes(e)
} catch (e) {
this.onerror(e)
}
}
async getDHTSettings() {
const e = (await this.local.get("@local/hyperdht")) || {}
return {
nodes: e.nodes || [],
concurrency: e.concurrency || Z.concurrency,
maxWindow: e.maxWindow || Z.maxWindow,
randomPunchInterval: e.randomPunchInterval || Z.randomPunchInterval,
connectionKeepAlive: e.connectionKeepAlive || Z.connectionKeepAlive,
}
}
async _setCachedDHTNodes(e) {
const t = await this.internalEnterLocalTransaction()
try {
const i = { ...(await this.getDHTSettings()), nodes: e }
;(await t.insert("@local/hyperdht", i), await t.flush())
} finally {
await this.internalExitLocalTransaction()
}
}
batch(e) {
return new We(this, e)
}
async dispatch(e, t, i) {
this.opened || (await this.ready())
const s = this.internalOpenExistingRoom(e)
try {
return await s.dispatch(t, i)
} catch (e) {
this.onerror(e)
} finally {
this.internalReleaseRoom(s)
}
}
async dispatchBatch(e, t) {
this.opened || (await this.ready())
const i = this.internalOpenExistingRoom(e)
try {
await i.dispatchBatch(t)
} finally {
this.internalReleaseRoom(i)
}
}
async selfDestruct({ confirm: e } = {}) {
if ("do not go gentle into that good night" !== e)
throw new Error("Must confirm self destruction")
;(await this.ready(), await this.close())
const t = Math.random().toString(16).slice(2),
i = K.join(this.dir, "gc", t)
try {
await P.promises.mkdir(i, { recursive: !0 })
} catch {}
for (const e of ["cores", "db", "cache", "app-references"])
try {
await P.promises.rename(K.join(this.dir, e), K.join(i, e))
} catch {}
await P.promises.rm(i, { recursive: !0 })
}
static from(e, t) {
const i = T.migrate(e),
s = i.active() || i.create({ name: "default" })
i.gc({ delay: ke })
const r = new this(s.storage, {
...t,
logout: async () => {
;(i.remove(s),
await new Promise((e) => setTimeout(e, 1e3)),
await r.close(),
i.gc({ delay: ke }))
},
})
return { profiles: i, keet: r }
}
static registerRPC(e, t, i) {
return new A(e, t, i)
}
}
const $e = {
interrupt: ze,
addWriter: ze,
removeWriter: ze,
ackWriter: ze,
preferFastForward: ze,
removeable: Qe,
createAnchor: (e, t) => ({ key: e, length: t }),
}
function Ge(e, t, i, s, r, o, { abi: a, trace: n, dryRun: l = !1 } = {}) {
return new ee.OpContext(e, t, {
fork: r.core.fork,
writer: { key: i.from.key, seq: i.length - 1 },
genesis: 0 === r.core.length,
bootstrap: s.bootstrap,
router: o,
hostcalls: l ? $e : s,
dryRun: l,
abi: a,
trace: n,
})
}
function He(t, i, s) {
const r = e.attestDevice(s, t.deviceKeyPair, t.deviceProof),
o = e.attestData(i, t.deviceKeyPair, t.deviceProof)
return { privateProfileIdentity: { memberKey: t.publicKey, proof: o }, deviceProof: r }
}
function Je(e) {
return (
"@keet/add-chat-message" === e ||
"@keet/start-call" === e ||
"@keet/join-call" === e ||
"@keet/leave-call" === e ||
"@keet/end-call" === e
)
}
function Ye(e) {
switch (e) {
case "nightly":
return ye.Nightly
case "internal":
return ye.Internal
case "production":
return ye.Production
default:
throw ge.INVALID_BUILD_TYPE()
}
}
function ze() {}
function Qe() {
return !0
}
function Xe(e, t) {
if (e.length !== t.length) return !1
for (let i = 0; i < e.length; i++) if (!k.equals(e[i], t[i])) return !1
return !0
}