• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LukaJCB / ts-mls / 20887934276

11 Jan 2026 02:12AM UTC coverage: 95.727% (+0.06%) from 95.665%
20887934276

push

github

LukaJCB
Update to vitest v4

409 of 417 branches covered (98.08%)

Branch coverage included in aggregate %.

2369 of 2485 relevant lines covered (95.33%)

80736.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.05
/src/codec/json.ts
1
import { ClientConfig } from "../clientConfig.js"
2
import { ClientState } from "../clientState.js"
3
import { EpochReceiverData } from "../epochReceiverData.js"
4

5
/**
6
 * @deprecated Use encodeGroupState instead for binary serialization
7
 */
8
export function toJsonString(clientState: ClientState): string {
9
  const { clientConfig, ...state } = clientState
19✔
10

11
  const stateWithSerializableMap = {
19✔
12
    ...state,
13
    historicalReceiverData: Array.from(state.historicalReceiverData.entries()).map(([epoch, data]) => [
×
14
      {
15
        epoch: epoch.toString(),
16
      },
17
      data,
18
    ]),
19
  }
20
  return JSON.stringify(stateWithSerializableMap, (_key, value: unknown) => {
19✔
21
    // Mark BigInt values with a special wrapper
22
    if (typeof value === "bigint") {
82,614✔
23
      return { "@@bigint": value.toString() }
57✔
24
    }
25
    // Mark empty Uint8Arrays with a special marker
26
    if (value instanceof Uint8Array) {
82,557✔
27
      if (value.length === 0) {
380✔
28
        return { "@@uint8array": true, length: 0, data: [] }
19✔
29
      }
30
    }
31
    return value
82,538✔
32
  })
33
}
34

35
function isValidGroupActiveState(state: unknown): boolean {
36
  if (typeof state !== "object" || state === null) return false
19✔
37
  const s = state as Record<string, unknown>
19✔
38
  if (typeof s.kind !== "string") return false
19✔
39
  if (s.kind === "active") return true
19✔
40
  if (s.kind === "suspendedPendingReinit") return "reinit" in s && typeof s.reinit === "object"
×
41
  if (s.kind === "removedFromGroup") return true
×
42
  return false
×
43
}
44

45
function isValidRatchetTree(tree: unknown): boolean {
46
  if (!Array.isArray(tree)) return false
19✔
47
  return tree.every((node) => node === null || (typeof node === "object" && node !== null))
19✔
48
}
49

50
function isValidGroupContext(ctx: unknown): boolean {
51
  if (typeof ctx !== "object" || ctx === null) return false
19✔
52
  const c = ctx as Record<string, unknown>
19✔
53
  return (
19✔
54
    "version" in c &&
55
    "cipherSuite" in c &&
56
    "groupId" in c &&
57
    "epoch" in c &&
58
    "treeHash" in c &&
59
    "confirmedTranscriptHash" in c &&
60
    "extensions" in c
61
  )
62
}
63

64
function isValidKeySchedule(ks: unknown): boolean {
65
  if (typeof ks !== "object" || ks === null) return false
19✔
66
  const k = ks as Record<string, unknown>
19✔
67
  return "epochAuthenticator" in k && typeof k.epochAuthenticator === "object"
19✔
68
}
69

70
function isValidPrivateKeyPath(pkp: unknown): boolean {
71
  if (typeof pkp !== "object" || pkp === null) return false
19✔
72
  const p = pkp as Record<string, unknown>
19✔
73
  return "leafIndex" in p && typeof p.leafIndex === "number"
19✔
74
}
75

76
function isValidUnappliedProposals(uap: unknown): boolean {
77
  return uap !== null && typeof uap === "object"
19✔
78
}
79

80
function isValidHistoricalReceiverData(hrd: unknown): boolean {
81
  if (!Array.isArray(hrd)) return false
19✔
82
  return hrd.every(
19✔
83
    (item: unknown) => Array.isArray(item) && item.length === 2 && typeof item[0] === "object" && "epoch" in item[0],
×
84
  )
85
}
86

87
function deepConvertUint8Arrays(obj: unknown, depth = 0, maxDepth = 20): unknown {
88
  if (depth > maxDepth) return obj
1,658✔
89
  if (obj === null || obj === undefined) return obj
1,658✔
90
  if (obj instanceof Uint8Array) return obj
1,658✔
91

92
  // Check for the special BigInt marker
93
  if (obj && typeof obj === "object" && "@@bigint" in obj) {
1,658✔
94
    const objRecord = obj as Record<string, unknown>
57✔
95
    if (typeof objRecord["@@bigint"] === "string") {
57✔
96
      return BigInt(objRecord["@@bigint"])
57✔
97
    }
98
  }
99

100
  // Check for the special empty Uint8Array marker
101
  if (obj && typeof obj === "object" && "@@uint8array" in obj) {
1,601✔
102
    const objRecord = obj as Record<string, unknown>
19✔
103
    if (objRecord["@@uint8array"] === true) {
19✔
104
      return new Uint8Array()
19✔
105
    }
106
  }
107

108
  // Handle non-empty Uint8Array-like objects
109
  if (obj && typeof obj === "object" && !Array.isArray(obj)) {
1,582✔
110
    const objRecord = obj as Record<string, unknown>
684✔
111
    const keys = Object.keys(objRecord)
684✔
112
    if (keys.length > 0 && !("@@uint8array" in objRecord) && !("@@bigint" in objRecord)) {
684✔
113
      // Check if all keys are numeric strings and all values are 0-255 numbers
114
      const allNumericKeys = keys.every((k) => /^\d+$/.test(k))
81,108✔
115
      if (allNumericKeys) {
627✔
116
        const allValidValues = keys.every(
380✔
117
          (k) =>
118
            Object.prototype.hasOwnProperty.call(objRecord, k) &&
80,861✔
119
            typeof objRecord[k] === "number" &&
120
            objRecord[k] >= 0 &&
121
            objRecord[k] <= 255,
122
        )
123
        if (allValidValues) {
380✔
124
          const numKeys = keys.map((k) => parseInt(k, 10))
80,842✔
125
          const values = numKeys.sort((a, b) => a - b).map((n) => objRecord[String(n)] as number)
80,842✔
126
          return new Uint8Array(values)
361✔
127
        }
128
      }
129
    }
130
  }
131

132
  if (Array.isArray(obj)) {
1,221✔
133
    return obj.map((item) => deepConvertUint8Arrays(item, depth + 1, maxDepth))
575✔
134
  }
135

136
  if (typeof obj === "object") {
1,031✔
137
    const objRecord = obj as Record<string, unknown>
323✔
138
    const result: Record<string, unknown> = {}
323✔
139
    for (const key in objRecord) {
323✔
140
      if (Object.prototype.hasOwnProperty.call(objRecord, key)) {
1,064✔
141
        result[key] = deepConvertUint8Arrays(objRecord[key], depth + 1, maxDepth)
1,064✔
142
      }
143
    }
144
    return result
323✔
145
  }
146

147
  return obj
708✔
148
}
149

150
/**
151
 * @deprecated Use decodeGroupState instead for binary deserialization
152
 */
153
export function fromJsonString(s: string, config: ClientConfig): ClientState | undefined {
154
  try {
19✔
155
    const parsed = JSON.parse(s) as unknown
19✔
156

157
    if (typeof parsed !== "object" || parsed === null) return undefined
19✔
158

159
    const parsedRecord = parsed as Record<string, unknown>
19✔
160
    if (
19✔
161
      !("groupActiveState" in parsedRecord) ||
162
      !("privatePath" in parsedRecord) ||
163
      !("ratchetTree" in parsedRecord) ||
164
      !("keySchedule" in parsedRecord) ||
165
      !("groupContext" in parsedRecord) ||
166
      !("unappliedProposals" in parsedRecord) ||
167
      !("signaturePrivateKey" in parsedRecord) ||
168
      !("confirmationTag" in parsedRecord) ||
169
      !("historicalReceiverData" in parsedRecord) ||
170
      !("secretTree" in parsedRecord)
171
    ) {
172
      return undefined
×
173
    }
174

175
    const converted = deepConvertUint8Arrays(parsedRecord) as Record<string, unknown>
19✔
176

177
    if (!isValidGroupActiveState(converted.groupActiveState)) return undefined
19✔
178
    if (!isValidPrivateKeyPath(converted.privatePath)) return undefined
19✔
179
    if (!isValidRatchetTree(converted.ratchetTree)) return undefined
19✔
180
    if (!isValidKeySchedule(converted.keySchedule)) return undefined
19✔
181
    if (!isValidGroupContext(converted.groupContext)) return undefined
19✔
182
    if (!isValidUnappliedProposals(converted.unappliedProposals)) return undefined
19✔
183
    if (!isValidHistoricalReceiverData(converted.historicalReceiverData)) return undefined
19✔
184

185
    if (!(converted.signaturePrivateKey instanceof Uint8Array || typeof converted.signaturePrivateKey === "object")) {
19✔
186
      return undefined
×
187
    }
188
    if (!(converted.confirmationTag instanceof Uint8Array || typeof converted.confirmationTag === "object")) {
19✔
189
      return undefined
×
190
    }
191

192
    // Reconstruct Map<bigint, EpochReceiverData>
193
    const historicalReceiverData = new Map<bigint, EpochReceiverData>()
19✔
194
    if (Array.isArray(converted.historicalReceiverData)) {
19✔
195
      for (const [keyObj, data] of converted.historicalReceiverData as [unknown, unknown][]) {
19✔
196
        if (keyObj && typeof keyObj === "object" && "epoch" in keyObj) {
×
197
          const keyObjRecord = keyObj as Record<string, unknown>
×
198
          if (typeof keyObjRecord.epoch === "bigint") {
×
199
            historicalReceiverData.set(keyObjRecord.epoch, data as EpochReceiverData)
×
200
          }
201
        }
202
      }
203
    }
204

205
    return { clientConfig: config, ...converted, historicalReceiverData } as unknown as ClientState
19✔
206
  } catch {
207
    return undefined
×
208
  }
209
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc