feat(license): add offline standalone activation#4318
Conversation
| fail(`Invalid offline license file: ${licensePath}`); | ||
| } | ||
|
|
||
| return parsed; |
There was a problem hiding this comment.
[Critical] readOfflineLicense() returns the raw parsed JSON where customerId is nested under .payload, but formatStandaloneArchiveName() accesses license.customerId at the top level. This causes sanitizeArchiveComponent(undefined) to crash with TypeError: Cannot read properties of undefined (reading 'replace') whenever --license-file is used.
Either reuse the readOfflineLicense from create-standalone-package.js (which already flattens correctly and is exported), or flatten the return value:
| return parsed; | |
| return { | |
| raw: parsed, | |
| customerId: parsed.payload.customerId, | |
| expiresAt: parsed.payload.expiresAt, | |
| }; |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| if (expiresAt.getTime() <= now.getTime()) { | ||
| throw new OfflineLicenseError('Offline license has expired.'); | ||
| } | ||
| if (!payload.features.includes(requiredFeature)) { |
There was a problem hiding this comment.
[Critical] crypto.verify() is called without try/catch. A corrupted or misformatted PEM key causes it to throw a raw OpenSSL error, which is NOT an OfflineLicenseError. In ensureOfflineLicense, the catch checks error instanceof OfflineLicenseError — the raw crypto error fails this check and is re-thrown, bypassing the interactive activation prompt entirely.
| if (!payload.features.includes(requiredFeature)) { | |
| function validateSignedLicense( | |
| license: SignedOfflineLicense, | |
| publicKeyPem: string, | |
| ): void { | |
| let valid: boolean; | |
| try { | |
| valid = verify( | |
| null, | |
| Buffer.from(canonicalJson(license.payload)), | |
| { key: publicKeyPem }, | |
| Buffer.from(license.signature.value, 'base64'), | |
| ); | |
| } catch { | |
| throw new OfflineLicenseError('Offline license signature is invalid.'); | |
| } | |
| if (!valid) { | |
| throw new OfflineLicenseError('Offline license signature is invalid.'); | |
| } | |
| } |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| licenseFingerprint: fingerprintLicense(license), | ||
| }; | ||
| await mkdir(path.dirname(options.activationPath), { recursive: true }); | ||
| await writeFile( |
There was a problem hiding this comment.
[Critical] Non-atomic writeFile for the activation file. If the process crashes mid-write (OOM, SIGKILL, power loss), activation.json becomes corrupt. On next startup, readActivation throws "Offline activation file is invalid JSON." — which does NOT match the "Offline license is not activated." string in ensureOfflineLicense, so the interactive re-activation prompt is never triggered. The user is permanently stuck.
Write to a temp file and rename atomically:
| await writeFile( | |
| const tmpPath = `${options.activationPath}.tmp`; | |
| await writeFile( | |
| tmpPath, | |
| `${JSON.stringify(activation, null, 2)}\n`, | |
| 'utf8', | |
| ); | |
| await rename(tmpPath, options.activationPath); |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| const interactive = options.interactive ?? isInteractive(); | ||
| if ( | ||
| !(error instanceof OfflineLicenseError) || | ||
| error.message !== 'Offline license is not activated.' || |
There was a problem hiding this comment.
[Critical] Exact string match on error message creates fragile cross-package coupling. If the core package ever rephrases this message (even a punctuation change), the interactive activation flow silently breaks — the user sees a raw error instead of the activation prompt, with no compile-time or test-time signal.
Add a machine-readable code property to OfflineLicenseError and match on that instead:
// In core:
export class OfflineLicenseError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
this.name = 'OfflineLicenseError';
}
}
// Here:
if (
!(error instanceof OfflineLicenseError) ||
error.code !== 'NOT_ACTIVATED' ||
!interactive
) {
throw error;
}— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| value: signature, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
[Critical] Zero log statements across the entire offline license flow (both core and CLI modules). Every failure — missing file, bad JSON, invalid signature, expired license, fingerprint mismatch, activation hash mismatch — surfaces only as a thrown error with a short string. When a customer reports "CLI won't start in our air-gapped environment," there is no diagnostic trail for the oncall engineer.
Add structured log lines at each decision point:
logger.info('offline-license: reading license file', { licensePath });
logger.info('offline-license: signature valid');
logger.info('offline-license: reading activation file', { activationPath });
logger.warn('offline-license: activation fingerprint mismatch');— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| // Offline License | ||
| // ============================================================================ | ||
|
|
||
| export * from './license/offline-license.js'; |
There was a problem hiding this comment.
[Suggestion] export * re-exports signOfflineLicensePayload — a vendor-side signing function that requires a private key — in the client-facing public API of @qwen-code/qwen-code-core. Any downstream package can import and misuse it.
Replace with named exports excluding the signing function:
export {
OFFLINE_LICENSE_VERSION,
OFFLINE_ACTIVATION_VERSION,
OfflineLicenseError,
verifyOfflineLicense,
activateOfflineLicense,
type OfflineLicensePayload,
type SignedOfflineLicense,
type OfflineActivation,
type OfflineLicenseStatus,
type VerifyOfflineLicenseOptions,
type ActivateOfflineLicenseOptions,
} from './license/offline-license.js';— qwen-latest-series-invite-beta-v34 via Qwen Code /review
|
|
||
|
|
||
| ============================================================ | ||
| @qwen-code/sdk@undefined |
There was a problem hiding this comment.
[Suggestion] @qwen-code/sdk@undefined with "License text not found." is a build-tooling artifact. The @undefined version and missing license text indicate the notices-generation tool failed to resolve the package. Fix the generation flow or remove this entry before shipping.
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| await writeFile( | ||
| options.activationPath, | ||
| `${JSON.stringify(activation, null, 2)}\n`, | ||
| 'utf8', |
There was a problem hiding this comment.
[Suggestion] Activation file is written without explicit mode: 0o600. On multi-user systems, the default permissions (typically 0o644 via umask) allow other local users to read the activation file containing customerId, activationHashSha256, and licenseFingerprint.
| 'utf8', | |
| `${JSON.stringify(activation, null, 2)}\n`, | |
| { encoding: 'utf8', mode: 0o600 }, |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| return { | ||
| raw: parsed, | ||
| customerId: parsed.payload.customerId, | ||
| expiresAt: parsed.payload.expiresAt, |
There was a problem hiding this comment.
[Suggestion] Build-time readOfflineLicense validates structure (shape, field types) but never calls crypto.verify() to check the signature against the embedded public key. A license with a structurally valid but cryptographically bogus signature passes all build-time checks and is only rejected post-install by the customer.
Consider verifying the signature here before returning, so the build pipeline catches signing mistakes before delivery.
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| return await verifyOfflineLicense(options); | ||
| } catch (error) { | ||
| const interactive = options.interactive ?? isInteractive(); | ||
| if ( |
There was a problem hiding this comment.
[Suggestion] Interactive recovery only triggers for the exact "not activated" error. If the activation file is corrupted (invalid JSON from partial write), tampered (invalid shape), or stale (fingerprint mismatch after license renewal), the error is re-thrown with no recovery prompt. The user is stuck with an opaque error and must manually locate and delete the activation file.
Consider broadening the recovery condition to cover any OfflineLicenseError during verification, or at minimum detect these cases and suggest deleting the activation file.
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
Co-authored-by: Greg Shikhman <[email protected]>
DragonnZhang
left a comment
There was a problem hiding this comment.
Second-pass review summary
Independently reviewed the full diff and corroborate the existing inline comments. The most impactful finding is a confirmed runtime bug in scripts/build-standalone-release.js: its readOfflineLicense() returns the raw parsed JSON (where customerId is nested under .payload), but the imported formatStandaloneArchiveName() accesses license.customerId at the top level. This will crash any release build that passes --license-file. The fix is straightforward — either flatten the return value (matching create-standalone-package.js's own readOfflineLicense) or import the latter.
Additional observations from this pass
-
Crypto error propagation —
crypto.verify()invalidateSignedLicensecan throw on malformed PEM keys (not just returnfalse). Wrapping it in try/catch and re-throwing asOfflineLicenseErrorwould keep the error taxonomy consistent and prevent the CLI catch block from mis-classifying the error. -
Activation atomicity — The activation file is written with a single non-atomic
writeFile. Writing to a temp file first and renaming would prevent corrupt-state scenarios on crash. -
Error coupling — The string-match guard (
error.message !== 'Offline license is not activated.') inensureOfflineLicenseis fragile. Adding an error code field (e.g.,error.code === 'NOT_ACTIVATED') toOfflineLicenseErrorwould decouple the packages cleanly. -
Public API surface —
signOfflineLicensePayload(a private-key signing function) is re-exported from@qwen-code/qwen-code-core's barrel. Consider a separateinternalexport path. -
Test coverage gaps — The core test suite covers the happy path and tamper detection well, but lacks branch coverage for
validatePayloadedge cases (zero/negative seats, unparseable expiry) and for fingerprint/customerId mismatch during verification.
Overall the architecture is sound — ed25519 signatures, canonical JSON for deterministic signing, SHA-256 digests for activation hashes, and clean separation between core verification and CLI interaction. The readOfflineLicense shape mismatch in the release script is the blocker; the remaining items are quality hardening.
— qwen-code via Qwen Code /review
DragonnZhang
left a comment
There was a problem hiding this comment.
Independent second review. The 13 existing inline comments from @wenshao comprehensively cover the key issues in this changeset — I independently verified each one and concur with the assessments. No additional high-confidence findings to add.
The most impactful items I confirmed through code analysis:
- The
readOfflineLicense()shape mismatch inbuild-standalone-release.js(returns raw JSON vs. structured object expected byformatStandaloneArchiveName) is a confirmed crash bug when packaging licensed releases. - The
crypto.verify()without try/catch, non-atomic activation file write, and user-editableenabledbypass are all valid Critical findings. - The
export *ofsignOfflineLicensePayloadin the core public API is a legitimate API hygiene concern.
Deterministic analysis: eslint clean, tsc timed out (120s).
— qwen3-coder via Qwen Code /review
|
@gxianch heads up — this PR currently has merge conflicts with Conflicting files:
The rest merges cleanly. Thanks! 中文@gxianch 提个醒 —— 这个 PR 目前和 冲突文件:
其余文件可以自动合并。谢谢! |
| if [[ -f "${INSTALL_LIB_DIR}/offline-license/license.json" ]]; then | ||
| local qwen_home_dir="${QWEN_HOME:-${HOME:-}/.qwen}" | ||
| mkdir -p "${qwen_home_dir}" | ||
| cp "${INSTALL_LIB_DIR}/offline-license/license.json" "${qwen_home_dir}/license.json" |
There was a problem hiding this comment.
[Bug] cp and mkdir -p above have no error checking. If either fails (permission denied, disk full, locked file), the installer continues to create_source_json and prints log_success "Qwen Code standalone archive installed successfully." — the user sees a successful install, then gets a confusing Offline license file is missing. error on first launch with no connection to the installer failure.
The rest of install_standalone follows a strict if ! ... ; then log_error ...; return 1; fi pattern for critical operations (e.g., the mv of the wrapper script above). The new license copy should follow the same convention:
if ! mkdir -p "${qwen_home_dir}" || ! cp "${INSTALL_LIB_DIR}/offline-license/license.json" "${qwen_home_dir}/license.json"; then
log_error "Failed to install offline license to ${qwen_home_dir}."
return 1
fiThe same issue exists in install-qwen-with-source.bat at line 620, where copy /Y failures are silently ignored (the batch script uses if !ERRORLEVEL! NEQ 0 checks elsewhere but not here).
|
@qwen-code /resolve |
|
@qwen-code /triage |
|
@qwen-code /resolve |
|
Thanks for the PR! Template: The PR body uses custom headings (Summary, Validation, Testing Matrix, Scope / Risk) instead of the required template headings (What this PR does, Why it's needed, Reviewer Test Plan, Tested on). The Chinese translation section is also missing. Please align with Problem: The linked issues (#8 "report error when try to auth", #9 "API Error: Streaming setup timeout", #10 "how to call Qwen3-Coder") are all unrelated to offline license activation. These appear to be random user support tickets, not issues that this PR actually closes. The PR doesn't link to any issue or design discussion that motivated this feature. Without that context, there's no way for reviewers to evaluate whether this is the right solution for the right problem. Direction: This PR introduces an entirely new licensing and activation enforcement system — ed25519 signature verification, first-launch activation prompts, local activation state persistence, and customer-specific release packaging. This is a significant security-sensitive feature that touches core infrastructure across Size: 409 production core lines (config.ts: 35, settingsSchema.ts: 64, core/index.ts: 6, core/offline-license.ts: 304), plus ~170 lines in build/install scripts. Test lines: ~600. Not blocking on size, but the cross-package scope (core + cli) and security-sensitive nature warrant maintainer review. Approach: A few concerns beyond the direction question:
Escalating to the maintainer for direction review before proceeding further. This PR introduces a licensing enforcement system that needs a product-level decision, not just a code review. @QwenLM/maintainers — this PR adds offline license enforcement (ed25519 signing, activation flow, customer-specific packaging). Needs a direction call before code review makes sense. 中文说明感谢贡献! 模板: PR 使用了自定义标题(Summary, Validation, Testing Matrix, Scope / Risk),而非模板要求的标题(What this PR does, Why it's needed, Reviewer Test Plan, Tested on)。缺少中文翻译部分。请参照 问题: 关联的 issues(#8 "认证报错"、#9 "Streaming 超时"、#10 "如何调用 Qwen3-Coder")均与离线许可证激活无关。这些看起来是随机的用户支持工单,并非此 PR 实际解决的问题。PR 没有关联任何讨论过此功能的 issue 或设计文档。没有这些上下文,审阅者无法判断这是否是正确的方案。 方向: 此 PR 引入了一个全新的许可证和激活执行系统——ed25519 签名验证、首次启动激活提示、本地激活状态持久化、以及客户特定的发布打包。这是一个重要的安全敏感功能,触及了 规模: 409 行生产核心代码(config.ts: 35, settingsSchema.ts: 64, core/index.ts: 6, core/offline-license.ts: 304),加上约 170 行构建/安装脚本。测试行约 600 行。规模本身不构成阻塞,但跨包范围(core + cli)和安全敏感性需要维护者审查。 方案: 除方向问题外的几个关注点:
转交维护者进行方向审查。此 PR 引入了许可证执行系统(ed25519 签名、激活流程、客户特定打包),需要产品层面的决策,而非仅仅是代码审查。 — Qwen Code · qwen3.7-max |
…vation Merged origin/main into qwen-resolve/pr-4318. Resolved 6 conflicted files: - packages/cli/src/config/config.ts: kept both sessionsCommand import and offline license imports - packages/cli/src/config/config.test.ts: kept HEAD offline license tests and main config tests - packages/vscode-ide-companion/NOTICES.txt: kept both notice blocks - scripts/create-standalone-package.js: kept all functions from both sides, merged duplicate exports - scripts/installation/INSTALLATION_GUIDE.md: kept offline customer delivery docs and hosted installer docs - scripts/tests/install-script.test.js: kept unique tests from both sides, deduplicated confused merges Co-authored-by: Qwen-Coder <[email protected]>
|
@qwen-code /resolve |
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge Conflict Resolution Summary — PR #4318Branch: Overview6 files had merge conflicts after Conflicted Files and Resolution1.
|
|
Qwen Code did not run conflict resolution for this request. PR #4318 does not currently have merge conflicts with main. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Review Summary
9 parallel review agents (Correctness, Security, Code Quality, Performance, Test Coverage, 3× Undirected Audit, Build & Test) + iterative reverse audit.
🔴 Critical (8)
-
Missing
});inconfig.test.ts— The new test block at line 1055 never closes thedescribe('loadCliConfig')block. Build is broken. Fix: add});after line 1055. -
No machine binding — The activation flow is portable across machines. Any machine with the license file + activation code can activate. Consider binding to a hardware fingerprint (MAC, hostname, or machine-id hash) in the
OfflineActivationrecord. -
publicKeyPemfrom user-editable settings — An attacker with write access tosettings.jsoncan supply their own public key, sign a forged license with their own private key, and bypass DRM entirely. The public key must come from a trusted source (embedded in the binary or a non-user-editable path). -
Installer integration gaps (3 issues):
install-qwen-standalone.shhas zero references to offline license files. The license copy logic was added only toinstall-qwen-with-source.sh, but tests userunUnixInstallerwhich invokes the standalone script.- Unconditional
cpoflicense.jsonover an existing installation leaves a staleactivation.jsonwith a mismatched fingerprint → next launch is blocked. - Installer copies license files but never sets
security.offlineLicense.enabled = truein user settings, so the feature never actually activates.
-
ensureOfflineLicensehappy path untested — No test covers the verify → prompt → activate flow or the non-interactive branch. -
readOfflineLicensePublicKeyfallback untested — ThereadFileSyncfallback inconfig.tsis never exercised by any test. -
Fingerprint hashes entire signed license —
fingerprintLicensehashes the fullSignedOfflineLicense(includingversionandsignature). If the vendor ever re-signs the same payload (e.g., version bump), existing activations break. Consider hashing onlypayload. (Low confidence — may be intentional.) -
OfflineLicenseStatussilently discarded —ensureOfflineLicensereturns status (customer ID, seats, features, expiry) butconfig.tsignores it. None of this information is logged or surfaced to the user.
🟡 Suggestions (5)
- Error messages lack diagnostic values — Pattern across
offline-license.ts: messages state what failed but not why (e.g., "has expired" without showing the expiry date or current time). - Array ordering not canonicalized —
sortForCanonicalJsonsorts object keys but preserves array order.features: ["a","b"]andfeatures: ["b","a"]produce different fingerprints. - Hardcoded
version: 1increate-standalone-package.js:443— should referenceOFFLINE_LICENSE_VERSIONconstant. ensureOfflineLicenseuses exact string match on error message'Offline license is not activated.'— fragile; consider an error code/subclass.- Barrel export of
offline-license.tsfrompackages/core/src/index.tsexposes internal license primitives to all consumers.
— qwen3.7-max via Qwen Code /review
| } as Settings, | ||
| {} as CliArgs, | ||
| ), | ||
| ).rejects.toThrow('Offline license file is missing.'); |
There was a problem hiding this comment.
🔴 Critical — Missing });
This test block is never closed. The describe('loadCliConfig') closing }); is missing after this line, which breaks the build (tsc reports "Expected } but found it").
Fix: Add }); after this line:
).rejects.toThrow('Offline license file is missing.');
}); // ← add this
it('should prefer CLI fallback models over settings fallback models', ...— qwen3.7-max via Qwen Code /review
| licenseFingerprint: fingerprintLicense(license), | ||
| }; | ||
| await mkdir(path.dirname(options.activationPath), { recursive: true }); | ||
| await writeFile( |
There was a problem hiding this comment.
🔴 Critical — No machine binding in activation
The activation record (OfflineActivation) is portable — it contains only customerId, activatedAt, activationHashSha256, and licenseFingerprint. Any machine that has the license file and the activation JSON can pass verification.
For a DRM feature targeting offline/air-gapped government customers, this means:
- An activated license can be copied to unauthorized machines
- There's no way to detect or prevent this
Suggestion: Bind activation to a machine fingerprint (hostname hash, machine-id, or MAC address hash) and verify it during verifyOfflineLicense. This doesn't need to be unbreakable — it raises the bar enough for the target deployment scenario.
— qwen3.7-max via Qwen Code /review
|
|
||
| const offlineLicense = settings.security?.offlineLicense; | ||
| if (offlineLicense?.enabled) { | ||
| const publicKeyPem = offlineLicense.publicKeyPem |
There was a problem hiding this comment.
🔴 Critical — Public key sourced from user-editable settings
offlineLicense.publicKeyPem comes from settings.json, which the user controls. An attacker can:
- Generate their own Ed25519 keypair
- Set
publicKeyPemto their public key insettings.json - Sign a forged license with their private key
- Activate with the forged license
The entire DRM scheme is bypassed because the trust anchor (public key) is user-controlled.
Fix: The public key must come from a source the user cannot modify — embed it in the binary, read it from a signed package, or at minimum only accept it from the public-key.pem file deployed by the installer (not from settings).
— qwen3.7-max via Qwen Code /review
|
|
||
| rm -rf "${old_install_dir}" | ||
| export PATH="${INSTALL_BIN_DIR}:${PATH}" | ||
| if [[ -f "${INSTALL_LIB_DIR}/offline-license/license.json" ]]; then |
There was a problem hiding this comment.
🔴 Critical — Three installer integration issues
-
Wrong installer modified. The license copy logic is added here (
install-qwen-with-source.sh), butinstall-qwen-standalone.sh— the script actually invoked byrunUnixInstallerin tests and by standalone deployments — has zero references to offline license files. License files are never deployed to$QWEN_HOME/in the standalone path. -
Stale activation on upgrade. This
cpunconditionally overwriteslicense.json, but the existingactivation.jsonin$QWEN_HOME/retains the oldlicenseFingerprint. If the vendor issues a new signed license (different signature, same payload), the fingerprint changes →verifyOfflineLicensethrows "activation does not match this license" → startup is blocked. -
Enforcement never enabled. The installer copies license files but never sets
security.offlineLicense.enabled = truein user settings. The feature is opt-in but the installer never opts in, so deployed licenses have no effect.
Fix: Add the same license copy logic to install-qwen-standalone.sh, handle the upgrade path (delete stale activation.json when license.json changes), and configure enabled: true during installation.
— qwen3.7-max via Qwen Code /review
| interactive?: boolean; | ||
| } | ||
|
|
||
| export async function ensureOfflineLicense( |
There was a problem hiding this comment.
🔴 Critical — ensureOfflineLicense happy path has no tests
This function orchestrates the core user-facing flow: try verify → catch "not activated" → prompt for activation code → activate. None of this is tested:
- Happy path (verify succeeds on first try)
- Activation flow (verify fails → prompt → activate succeeds)
- Non-interactive mode (verify fails → re-throw without prompting)
- Cancelled prompt (empty activation code)
- Fragile string match on
error.message !== 'Offline license is not activated.'
This is the most complex new orchestration code in the PR and should have unit tests.
— qwen3.7-max via Qwen Code /review
| } | ||
| } | ||
|
|
||
| function readOfflineLicensePublicKey(filePath: string): string { |
There was a problem hiding this comment.
🔴 Critical — readOfflineLicensePublicKey fallback untested
This function reads a PEM file synchronously with readFileSync and has a catch-all error handler that swallows all errors (including permission errors, not just missing files). No test exercises this path.
Additionally, the catch-all means a permission error on the key file produces the same message as a missing file, masking real issues.
— qwen3.7-max via Qwen Code /review
| return sha256Hex(canonicalJson(license)); | ||
| } | ||
|
|
||
| function sha256Hex(value: string): string { |
There was a problem hiding this comment.
🟡 Suggestion — Fingerprint hashes the entire signed license, not just the payload
fingerprintLicense calls canonicalJson(license) on the full SignedOfflineLicense, which includes version and signature. If the vendor ever re-signs the same payload (e.g., after a version bump or key rotation), the fingerprint changes and existing activations break.
Consider hashing only the payload:
function fingerprintLicense(license: SignedOfflineLicense): string {
return sha256Hex(canonicalJson(license.payload));
}(Low confidence — this may be intentional to detect any modification.)
— qwen3.7-max via Qwen Code /review
| throw new OfflineLicenseError('Offline license expiry is invalid.'); | ||
| } | ||
| if (expiresAt.getTime() <= now.getTime()) { | ||
| throw new OfflineLicenseError('Offline license has expired.'); |
There was a problem hiding this comment.
🟡 Suggestion — Error messages lack diagnostic context
Pattern across this file: error messages state what failed but not the values that caused the failure. Examples:
'Offline license has expired.'— doesn't show the expiry date or current time'Offline license seat count is invalid.'— doesn't show the actual value'Offline license activation code is invalid.'— doesn't distinguish between wrong code and wrong license
Adding diagnostic values (not secrets) helps operators debug deployment issues:
throw new OfflineLicenseError(
`Offline license has expired (expired at ${payload.expiresAt}).`,
);— qwen3.7-max via Qwen Code /review
| return value.map(sortForCanonicalJson); | ||
| } | ||
| if (value && typeof value === 'object') { | ||
| const record = value as Record<string, unknown>; |
There was a problem hiding this comment.
🟡 Suggestion — Array ordering not canonicalized
sortForCanonicalJson recursively sorts object keys but preserves array order. This means:
{ "features": ["agent-cli", "web-shell"] }
{ "features": ["web-shell", "agent-cli"] }produce different canonical JSON strings and therefore different fingerprints. If two vendors (or two versions of the signing tool) emit features in different orders, the fingerprint won't match even though the payload is semantically identical.
Consider sorting arrays of primitives in sortForCanonicalJson:
if (Array.isArray(value)) {
const sorted = value.map(sortForCanonicalJson);
return sorted.every((v) => typeof v === 'string' || typeof v === 'number')
? sorted.sort()
: sorted;
}— qwen3.7-max via Qwen Code /review
| : readOfflineLicensePublicKey( | ||
| path.join(Storage.getGlobalQwenDir(), 'public-key.pem'), | ||
| ); | ||
| await ensureOfflineLicense({ |
There was a problem hiding this comment.
🟡 Suggestion — OfflineLicenseStatus silently discarded
ensureOfflineLicense returns an OfflineLicenseStatus with customerId, seats, features, and expiresAt — but the return value is ignored here. None of this information is logged or made available to the user.
Consider at minimum logging the customer ID and expiry date at debug level, so operators can confirm which license is active:
const status = await ensureOfflineLicense({ ... });
debugLogger.debug(`Offline license active: ${status.customerId}, expires ${status.expiresAt}`);— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
Summary
Validation
Scope / Risk
Testing Matrix
Testing matrix notes:
Linked Issues / Bugs