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

LukaJCB / ts-mls / 20016537925

08 Dec 2025 04:18AM UTC coverage: 96.423% (-0.5%) from 96.901%
20016537925

push

github

web-flow
Add json serialization (#167)

1207 of 1350 branches covered (89.41%)

Branch coverage included in aggregate %.

145 of 168 new or added lines in 1 file covered. (86.31%)

6825 of 6980 relevant lines covered (97.78%)

45452.75 hits per line

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

79.57
/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
    // Mark BigInt values with a special wrapper
18
    if (typeof value === "bigint") {
84,743✔
19
      return { "@@bigint": value.toString() }
57✔
20
    }
57✔
21
    // Mark empty Uint8Arrays with a special marker
22
    if (value instanceof Uint8Array) {
84,743✔
23
      if (value.length === 0) {
418✔
24
        return { "@@uint8array": true, length: 0, data: [] }
19✔
25
      }
19✔
26
    }
418✔
27
    return value
84,667✔
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,739✔
84
  if (depth > maxDepth) return obj
1,739!
85
  if (obj === null || obj === undefined) return obj
1,739!
86
  if (obj instanceof Uint8Array) return obj
1,739!
87

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

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

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

128
  if (Array.isArray(obj)) {
1,739✔
129
    return obj.map((item) => deepConvertUint8Arrays(item, depth + 1, maxDepth))
190✔
130
  }
190✔
131

132
  if (typeof obj === "object") {
1,739✔
133
    const objRecord = obj as Record<string, unknown>
342✔
134
    const result: Record<string, unknown> = {}
342✔
135
    for (const key in objRecord) {
342✔
136
      if (Object.prototype.hasOwnProperty.call(objRecord, key)) {
1,140✔
137
        result[key] = deepConvertUint8Arrays(objRecord[key], depth + 1, maxDepth)
1,140✔
138
      }
1,140✔
139
    }
1,140✔
140
    return result
342✔
141
  }
342✔
142

143
  return obj
732✔
144
}
732✔
145

146
export function fromJsonString(s: string, config: ClientConfig): ClientState | undefined {
1✔
147
  try {
19✔
148
    const parsed = JSON.parse(s) as unknown
19✔
149

150
    if (typeof parsed !== "object" || parsed === null) return undefined
19!
151

152
    const parsedRecord = parsed as Record<string, unknown>
19✔
153
    if (
19✔
154
      !("groupActiveState" in parsedRecord) ||
19✔
155
      !("privatePath" in parsedRecord) ||
19✔
156
      !("ratchetTree" in parsedRecord) ||
19✔
157
      !("keySchedule" in parsedRecord) ||
19✔
158
      !("groupContext" in parsedRecord) ||
19✔
159
      !("unappliedProposals" in parsedRecord) ||
19✔
160
      !("signaturePrivateKey" in parsedRecord) ||
19✔
161
      !("confirmationTag" in parsedRecord) ||
19✔
162
      !("historicalReceiverData" in parsedRecord) ||
19✔
163
      !("secretTree" in parsedRecord)
19✔
164
    ) {
19!
NEW
165
      return undefined
×
NEW
166
    }
×
167

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

170
    if (!isValidGroupActiveState(converted.groupActiveState)) return undefined
19!
171
    if (!isValidPrivateKeyPath(converted.privatePath)) return undefined
19!
172
    if (!isValidRatchetTree(converted.ratchetTree)) return undefined
19!
173
    if (!isValidKeySchedule(converted.keySchedule)) return undefined
19!
174
    if (!isValidGroupContext(converted.groupContext)) return undefined
19!
175
    if (!isValidUnappliedProposals(converted.unappliedProposals)) return undefined
19!
176
    if (!isValidHistoricalReceiverData(converted.historicalReceiverData)) return undefined
19!
177

178
    if (!(converted.signaturePrivateKey instanceof Uint8Array || typeof converted.signaturePrivateKey === "object")) {
19!
NEW
179
      return undefined
×
NEW
180
    }
×
181
    if (!(converted.confirmationTag instanceof Uint8Array || typeof converted.confirmationTag === "object")) {
19!
NEW
182
      return undefined
×
NEW
183
    }
×
184

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

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