Files
dc/client/src/app/licensing/utils/combinedLicenceKey.ts
T
hermes 8e51fde524
Build / Build-and-ng-test (pull_request) Successful in 5m9s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m31s
Build / Build-and-test-development (pull_request) Failing after 33m53s
fix(licensing): bridge async proceed() through cy.then and replace Response.arrayBuffer with manual stream reader
- licensing.cy.ts: call proceed() unconditionally via cy.then() instead of conditionally chaining .then() after cy.wait(), so the command queue stays open across the async gap and enqueued commands always run
- combinedLicenceKey.ts: replace new Response(stream).arrayBuffer() with an explicit streamToArrayBuffer helper for broader browser support
- remove debug-license.cy.ts, an untracked throwaway repro for the promise-handling question
2026-08-29 20:13:22 +01:00

84 lines
2.7 KiB
TypeScript

import * as base64Converter from 'base64-arraybuffer'
/**
* Format produced by dckey's own encodeCombinedKey() (main.js):
* "DCKEY1:" + base64(gzip(licenceKey + " " + activationKey)). The prefix is
* plain ASCII, not itself compressed/encoded - base64's alphabet never
* contains ":", so it can never collide with the start of a legacy licence
* key or activation key, making format detection unambiguous.
*/
export const COMBINED_KEY_PREFIX = 'DCKEY1:'
export const isCombinedLicenceKey = (text: string): boolean =>
text.trim().startsWith(COMBINED_KEY_PREFIX)
const streamToArrayBuffer = async (
readable: ReadableStream<Uint8Array>
): Promise<ArrayBuffer> => {
const reader = readable.getReader()
const chunks: Uint8Array[] = []
let totalLength = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
totalLength += value.length
}
const result = new Uint8Array(totalLength)
let offset = 0
for (const chunk of chunks) {
result.set(chunk, offset)
offset += chunk.length
}
return result.buffer
}
const gzipDecompress = async (bytes: ArrayBuffer): Promise<ArrayBuffer> => {
const decompressionStream = new DecompressionStream('gzip')
const writer = decompressionStream.writable.getWriter()
// Awaited (unlike a fire-and-forget write) so invalid gzip data rejects
// through this function's own returned promise, not as a separate
// unhandled rejection racing the readable side below.
const result = await Promise.all([
writer.write(new Uint8Array(bytes)).then(() => writer.close()),
streamToArrayBuffer(decompressionStream.readable)
])
return result[1]
}
/**
* Splits a combined licence key back into its two parts. Returns null (not
* a rejected promise) when text isn't a combined key at all, so callers can
* use it as a "try this, then fall back to the legacy two-field input" check
* without a try/catch for that common case - a prefixed-but-corrupted
* string still rejects, since that's a real error, not a format mismatch.
*/
export const splitCombinedLicenceKey = async (
text: string
): Promise<{ licenceKey: string; activationKey: string } | null> => {
const trimmed = text.trim()
if (!isCombinedLicenceKey(trimmed)) return null
const compressedBytes = base64Converter.decode(
trimmed.slice(COMBINED_KEY_PREFIX.length)
)
const decompressedBytes = await gzipDecompress(compressedBytes)
const payload = new TextDecoder().decode(decompressedBytes)
const separatorIndex = payload.indexOf(' ')
if (separatorIndex === -1) {
throw new Error('Invalid combined licence key: missing separator')
}
return {
licenceKey: payload.slice(0, separatorIndex),
activationKey: payload.slice(separatorIndex + 1)
}
}