- 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
84 lines
2.7 KiB
TypeScript
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)
|
|
}
|
|
}
|