Skip to content

Commit f372289

Browse files
sunnylqmclaude
andcommitted
fix(publish): never send JSON null for Hermes chain metadata
Servers treat an explicit null in the optional string fields as invalid ("Expected baseHash string" on 2.22.0/2.22.1 when no base was used). Send only the fields that are known; absent fields are accepted by strict, tolerant and pre-2.22 servers alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b8a98a4 commit f372289

2 files changed

Lines changed: 42 additions & 4 deletions

File tree

src/versions.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,23 +468,30 @@ async function describePpkBundle(
468468
ppkPath: string,
469469
base?: HermesBaseMeta,
470470
): Promise<Record<string, unknown>> {
471+
// Only known values are sent: the server's optional-field parsers treat an
472+
// explicit JSON null as invalid (that contract broke 2.22.0/2.22.1 publishes
473+
// with "Expected baseHash string"), and omitting a field is what every
474+
// server version — strict, tolerant or too old to know the field — accepts.
471475
const meta: Record<string, unknown> = {};
472476
try {
473477
const bundle = await extractBundleFromArchive(ppkPath);
474478
if (bundle) {
475479
meta.bundleHash = sha256Hex(bundle);
476-
const hbcVersion = getHbcVersion(bundle);
477-
meta.bytecodeVersion = hbcVersion ?? base?.bytecodeVersion ?? null;
480+
const hbcVersion = getHbcVersion(bundle) ?? base?.bytecodeVersion;
481+
if (hbcVersion != null) meta.bytecodeVersion = hbcVersion;
478482
await cachePut(bundle).catch(() => {});
479483
}
480484
} catch {
481485
// best effort: metadata never blocks a publish
482486
}
483-
meta.baseVersionId = base?.baseVersionId ?? null;
484-
meta.baseHash = base?.baseHash ?? null;
487+
if (base?.baseVersionId != null) meta.baseVersionId = base.baseVersionId;
488+
if (base?.baseHash) meta.baseHash = base.baseHash;
485489
return meta;
486490
}
487491

492+
/** Exported for tests: the version/create fields derived from a ppk + base. */
493+
export const describePpkBundleForTests = describePpkBundle;
494+
488495
export const versionCommands = {
489496
publish: async ({
490497
args,

tests/hermes-base.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,3 +526,34 @@ describe.if(hasHermesc)('with a real hermesc', () => {
526526
);
527527
});
528528
});
529+
530+
describe('publish metadata never sends JSON null', () => {
531+
test('describePpkBundle omits unknown chain fields', async () => {
532+
const { describePpkBundleForTests } = await import('../src/versions');
533+
const dir = mkTemp('rnu-publish-meta-');
534+
try {
535+
const ppk = path.join(dir, 'v.ppk');
536+
await writeZip(ppk, { 'index.bundlejs': fakeHbc(98, 'meta') });
537+
const noBase = await describePpkBundleForTests(ppk, undefined);
538+
expect(noBase.bundleHash).toBe(sha256Hex(fakeHbc(98, 'meta')));
539+
expect(noBase.bytecodeVersion).toBe(98);
540+
expect('baseVersionId' in noBase).toBe(false);
541+
expect('baseHash' in noBase).toBe(false);
542+
expect(Object.values(noBase).some((v) => v === null)).toBe(false);
543+
const withBase = await describePpkBundleForTests(ppk, {
544+
bytecodeVersion: 98,
545+
baseVersionId: 7,
546+
baseHash: 'objkey',
547+
});
548+
expect(withBase.baseVersionId).toBe(7);
549+
expect(withBase.baseHash).toBe('objkey');
550+
// plain JS bundle: no bytecodeVersion at all rather than null
551+
const js = path.join(dir, 'js.ppk');
552+
await writeZip(js, { 'index.bundlejs': Buffer.from('var a = 1;') });
553+
const plain = await describePpkBundleForTests(js, undefined);
554+
expect('bytecodeVersion' in plain).toBe(false);
555+
} finally {
556+
fs.removeSync(dir);
557+
}
558+
});
559+
});

0 commit comments

Comments
 (0)