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

LukaJCB / ts-mls / 20016040299

08 Dec 2025 03:48AM UTC coverage: 96.409% (-0.5%) from 96.901%
20016040299

Pull #167

github

web-flow
Merge 63288ec34 into 107913a14
Pull Request #167: Add json serialization

1205 of 1348 branches covered (89.39%)

Branch coverage included in aggregate %.

143 of 167 new or added lines in 1 file covered. (85.63%)

6823 of 6979 relevant lines covered (97.76%)

45255.51 hits per line

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

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

4
export function toJsonString(clientState: ClientState): string {
1✔
5
  const { clientConfig, ...state } = clientState
19✔
6

7
  const stateWithSerializableMap = {
19✔
8
    ...state,
19✔
9
    historicalReceiverData: Array.from(state.historicalReceiverData.entries()).map(([epoch, data]) => [
19✔
NEW
10
      {
×
NEW
11
        epoch: epoch.toString(),
×
NEW
12
      },
×
NEW
13
      data,
×
14
    ]),
19✔
15
  }
19✔
16
  return JSON.stringify(stateWithSerializableMap, (_key, value: unknown) => {
19✔
17
    // convert all BigInt values to strings
18
    if (typeof value === "bigint") {
84,686✔
19
      return value.toString()
57✔
20
    }
57✔
21
    // Mark empty Uint8Arrays with a special marker
22
    if (value instanceof Uint8Array) {
84,686✔
23
      if (value.length === 0) {
418✔
24
        return { "@@uint8array": true, length: 0, data: [] }
19✔
25
      }
19✔
26
    }
418✔
27
    return value
84,610✔
28
  })
19✔
29
}
19✔
30

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

41
function isValidRatchetTree(tree: unknown): boolean {
19✔
42
  if (!Array.isArray(tree)) return false
19!
43
  return tree.every((node) => node === null || (typeof node === "object" && node !== null))
19✔
44
}
19✔
45

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

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

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

72
function isValidUnappliedProposals(uap: unknown): boolean {
19✔
73
  return uap !== null && typeof uap === "object"
19✔
74
}
19✔
75

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

83
function deepConvertUint8Arrays(obj: unknown, depth = 0, maxDepth = 20): unknown {
1,740✔
84
  if (depth > maxDepth) return obj
1,740!
85
  if (obj === null || obj === undefined) return obj
1,740!
86
  if (obj instanceof Uint8Array) return obj
1,740!
87

88
  // Check for the special empty Uint8Array marker
89
  if (obj && typeof obj === "object" && "@@uint8array" in obj) {
1,740✔
90
    const objRecord = obj as Record<string, unknown>
19✔
91
    if (objRecord["@@uint8array"] === true) {
19✔
92
      return new Uint8Array()
19✔
93
    }
19✔
94
  }
19✔
95

96
  // Handle non-empty Uint8Array-like objects
97
  if (obj && typeof obj === "object" && !Array.isArray(obj)) {
1,740✔
98
    const objRecord = obj as Record<string, unknown>
741✔
99
    const keys = Object.keys(objRecord)
741✔
100
    if (keys.length > 0 && !("@@uint8array" in objRecord)) {
741✔
101
      // Check if all keys are numeric strings and all values are 0-255 numbers
102
      const allNumericKeys = keys.every((k) => /^\d+$/.test(k))
684✔
103
      if (allNumericKeys) {
684✔
104
        const allValidValues = keys.every(
418✔
105
          (k) =>
418✔
106
            Object.prototype.hasOwnProperty.call(objRecord, k) &&
82,908✔
107
            typeof objRecord[k] === "number" &&
82,908✔
108
            objRecord[k] >= 0 &&
82,889✔
109
            objRecord[k] <= 255,
82,889✔
110
        )
418✔
111
        if (allValidValues) {
418✔
112
          const numKeys = keys.map((k) => parseInt(k, 10))
399✔
113
          const values = numKeys.sort((a, b) => a - b).map((n) => objRecord[String(n)] as number)
399✔
114
          return new Uint8Array(values)
399✔
115
        }
399✔
116
      }
418✔
117
    }
684✔
118
  }
741✔
119

120
  if (Array.isArray(obj)) {
1,740✔
121
    return obj.map((item) => deepConvertUint8Arrays(item, depth + 1, maxDepth))
190✔
122
  }
190✔
123

124
  if (typeof obj === "object") {
1,740✔
125
    const objRecord = obj as Record<string, unknown>
342✔
126
    const result: Record<string, unknown> = {}
342✔
127
    for (const key in objRecord) {
342✔
128
      if (Object.prototype.hasOwnProperty.call(objRecord, key)) {
1,140✔
129
        result[key] = deepConvertUint8Arrays(objRecord[key], depth + 1, maxDepth)
1,140✔
130
      }
1,140✔
131
    }
1,140✔
132
    return result
342✔
133
  }
342✔
134

135
  return obj
790✔
136
}
790✔
137

138
export function fromJsonString(s: string, config: ClientConfig): ClientState | undefined {
1✔
139
  try {
19✔
140
    const parsed = JSON.parse(s) as unknown
19✔
141

142
    if (typeof parsed !== "object" || parsed === null) return undefined
19!
143

144
    const parsedRecord = parsed as Record<string, unknown>
19✔
145
    if (
19✔
146
      !("groupActiveState" in parsedRecord) ||
19✔
147
      !("privatePath" in parsedRecord) ||
19✔
148
      !("ratchetTree" in parsedRecord) ||
19✔
149
      !("keySchedule" in parsedRecord) ||
19✔
150
      !("groupContext" in parsedRecord) ||
19✔
151
      !("unappliedProposals" in parsedRecord) ||
19✔
152
      !("signaturePrivateKey" in parsedRecord) ||
19✔
153
      !("confirmationTag" in parsedRecord) ||
19✔
154
      !("historicalReceiverData" in parsedRecord) ||
19✔
155
      !("secretTree" in parsedRecord)
19✔
156
    ) {
19!
NEW
157
      return undefined
×
NEW
158
    }
×
159

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

162
    if (!isValidGroupActiveState(converted.groupActiveState)) return undefined
19!
163
    if (!isValidPrivateKeyPath(converted.privatePath)) return undefined
19!
164
    if (!isValidRatchetTree(converted.ratchetTree)) return undefined
19!
165
    if (!isValidKeySchedule(converted.keySchedule)) return undefined
19!
166
    if (!isValidGroupContext(converted.groupContext)) return undefined
19!
167
    if (!isValidUnappliedProposals(converted.unappliedProposals)) return undefined
19!
168
    if (!isValidHistoricalReceiverData(converted.historicalReceiverData)) return undefined
19!
169

170
    if (!(converted.signaturePrivateKey instanceof Uint8Array || typeof converted.signaturePrivateKey === "object")) {
19!
NEW
171
      return undefined
×
NEW
172
    }
×
173
    if (!(converted.confirmationTag instanceof Uint8Array || typeof converted.confirmationTag === "object")) {
19!
NEW
174
      return undefined
×
NEW
175
    }
×
176

177
    // Convert string BigInt values back to actual BigInts
178
    const groupContext = converted.groupContext as Record<string, unknown>
19✔
179
    if (groupContext && typeof groupContext.epoch === "string") {
19✔
180
      groupContext.epoch = BigInt(groupContext.epoch)
19✔
181
    }
19✔
182

183
    // Reconstruct Map<bigint, EpochReceiverData>
184
    const historicalReceiverData = new Map<bigint, EpochReceiverData>()
19✔
185
    if (Array.isArray(converted.historicalReceiverData)) {
19✔
186
      for (const [keyObj, data] of converted.historicalReceiverData as [unknown, unknown][]) {
19!
NEW
187
        if (keyObj && typeof keyObj === "object" && "epoch" in keyObj) {
×
NEW
188
          const keyObjRecord = keyObj as Record<string, unknown>
×
NEW
189
          if (typeof keyObjRecord.epoch === "string") {
×
NEW
190
            const epoch = BigInt(keyObjRecord.epoch)
×
NEW
191
            historicalReceiverData.set(epoch, data as EpochReceiverData)
×
NEW
192
          }
×
NEW
193
        }
×
NEW
194
      }
×
195
    }
19✔
196

197
    return { config, ...converted, historicalReceiverData } as unknown as ClientState
19✔
198
  } catch {
19!
NEW
199
    return undefined
×
NEW
200
  }
×
201
}
19✔
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