From 357f7885cad2c546bf8e986ed4a26fac6ff90183 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 29 Oct 2026 20:14:25 -0700 Subject: [PATCH] Validate required preconditions on binding updates --- CHANGELOG.md | 4 ++++ examples/catalog/package.json | 2 +- examples/docs/package.json | 2 +- package.json | 2 +- packages/browser/README.md | 2 +- packages/browser/package.json | 2 +- packages/cli/README.md | 2 +- packages/cli/package.json | 2 +- packages/cli/src/version.ts | 2 +- packages/console/package.json | 2 +- packages/contracts/README.md | 2 +- packages/contracts/package.json | 2 +- packages/react/README.md | 2 +- packages/react/package.json | 2 +- packages/runtime/README.md | 2 +- packages/runtime/package.json | 2 +- packages/runtime/src/index.ts | 4 ++++ packages/test/README.md | 2 +- packages/test/package.json | 2 +- tests/generator.test.mjs | 4 ++-- tests/runtime.test.mjs | 35 +++++++++++++++++++++++++++++++++ 21 files changed, 62 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d8801..922cb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.2 + +- Reject an updated binding that omits a required precondition, preserving the previous working binding. + ## 1.0.1 - Keep project-file reads tied to the checked descriptor and reject a swapped path before generating or applying an integration. diff --git a/examples/catalog/package.json b/examples/catalog/package.json index a6aa5ea..2e2ac8f 100644 --- a/examples/catalog/package.json +++ b/examples/catalog/package.json @@ -1,7 +1,7 @@ { "name": "latch-example-catalog", "private": true, - "version": "1.0.1", + "version": "1.0.2", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1 --port 5173 --strictPort", diff --git a/examples/docs/package.json b/examples/docs/package.json index 42d52df..57c7bf2 100644 --- a/examples/docs/package.json +++ b/examples/docs/package.json @@ -1,7 +1,7 @@ { "name": "latch-example-docs", "private": true, - "version": "1.0.1", + "version": "1.0.2", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1 --port 5174 --strictPort", diff --git a/package.json b/package.json index 7c1cc84..8a06beb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "latch-release-workspace", - "version": "1.0.1", + "version": "1.0.2", "private": true, "type": "module", "packageManager": "pnpm@10.17.1", diff --git a/packages/browser/README.md b/packages/browser/README.md index 795cfe9..7f2d1ff 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -1,4 +1,4 @@ -# @latch-local/browser 1.0.1 +# @latch-local/browser 1.0.2 Part of Latch: Connect your site. Verify every action. diff --git a/packages/browser/package.json b/packages/browser/package.json index 319406d..24f4e14 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@latch-local/browser", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "description": "Latch browser: Connect your site. Verify every action.", "license": "MIT", diff --git a/packages/cli/README.md b/packages/cli/README.md index 1c500ae..f590c72 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,4 +1,4 @@ -# @latch-local/cli 1.0.1 +# @latch-local/cli 1.0.2 Part of Latch: Connect your site. Verify every action. diff --git a/packages/cli/package.json b/packages/cli/package.json index 3b0ae37..29c59de 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@latch-local/cli", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "description": "Latch cli: Connect your site. Verify every action.", "license": "MIT", diff --git a/packages/cli/src/version.ts b/packages/cli/src/version.ts index e96d287..926d3f2 100644 --- a/packages/cli/src/version.ts +++ b/packages/cli/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.1'; +export const VERSION = '1.0.2'; diff --git a/packages/console/package.json b/packages/console/package.json index e40f88d..7b46021 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@latch-local/console-source", - "version": "1.0.1", + "version": "1.0.2", "private": true, "type": "module", "dependencies": { diff --git a/packages/contracts/README.md b/packages/contracts/README.md index ad548d2..d36e4fb 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -1,4 +1,4 @@ -# @latch-local/contracts 1.0.1 +# @latch-local/contracts 1.0.2 Part of Latch: Connect your site. Verify every action. diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 6cfe9f4..dc54f1a 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@latch-local/contracts", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "description": "Latch contracts: Connect your site. Verify every action.", "license": "MIT", diff --git a/packages/react/README.md b/packages/react/README.md index f079cc6..994dae2 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1,4 +1,4 @@ -# @latch-local/react 1.0.1 +# @latch-local/react 1.0.2 Part of Latch: Connect your site. Verify every action. diff --git a/packages/react/package.json b/packages/react/package.json index 8ead9a3..5d307ad 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@latch-local/react", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "description": "Latch react: Connect your site. Verify every action.", "license": "MIT", diff --git a/packages/runtime/README.md b/packages/runtime/README.md index ba6c757..9783d37 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -1,4 +1,4 @@ -# @latch-local/runtime 1.0.1 +# @latch-local/runtime 1.0.2 Part of Latch: Connect your site. Verify every action. diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 28d3df8..8eea6aa 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@latch-local/runtime", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "description": "Latch runtime: Connect your site. Verify every action.", "license": "MIT", diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index d7ed024..6a6e95d 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -154,6 +154,10 @@ export function createScope(bindings: Binding[], options: ScopeOptions = {}) { ) ) throw new LatchError('BINDING', 'Contracts changed: remount the scope'); + for (const b of next) + for (const p of b.contract.preconditions) + if (typeof b.preconditions?.[p] !== 'function') + throw new LatchError('PRECONDITION', `${b.contract.name}: missing precondition ${p}`); current = next; }, dispose() { diff --git a/packages/test/README.md b/packages/test/README.md index 226d385..b670116 100644 --- a/packages/test/README.md +++ b/packages/test/README.md @@ -1,4 +1,4 @@ -# @latch-local/test 1.0.1 +# @latch-local/test 1.0.2 Part of Latch: Connect your site. Verify every action. diff --git a/packages/test/package.json b/packages/test/package.json index e7d5215..dcce5ae 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@latch-local/test", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "description": "Latch test: Connect your site. Verify every action.", "license": "MIT", diff --git a/tests/generator.test.mjs b/tests/generator.test.mjs index 0882f60..28f246b 100644 --- a/tests/generator.test.mjs +++ b/tests/generator.test.mjs @@ -66,14 +66,14 @@ test('a valid 0.1.0 ownership manifest upgrades and keeps rollback protection', generate(r); apply(r); const output = path.join(r, 'src/latch.generated/integration.ts'); - fs.writeFileSync(output, fs.readFileSync(output, 'utf8').replace('Latch 1.0.1.', 'Latch 0.1.0.')); + fs.writeFileSync(output, fs.readFileSync(output, 'utf8').replace('Latch 1.0.2.', 'Latch 0.1.0.')); const manifestPath = path.join(r, '.latch/manifest.json'); const old = JSON.parse(fs.readFileSync(manifestPath)); old.generatorVersion = '0.1.0'; old.files['src/latch.generated/integration.ts'].after = createHash('sha256').update(fs.readFileSync(output)).digest('hex'); fs.writeFileSync(manifestPath, JSON.stringify(old)); generate(r); - assert.equal(JSON.parse(fs.readFileSync(manifestPath)).generatorVersion, '1.0.1'); + assert.equal(JSON.parse(fs.readFileSync(manifestPath)).generatorVersion, '1.0.2'); assert.equal(check(r).installation, 'applied'); rollback(r); assert.equal(fs.readFileSync(path.join(r, 'src/main.tsx'), 'utf8'), before); diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 61ea2c5..85589dd 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -194,6 +194,41 @@ test('preconditions are evaluated at execution time', async () => { assert.deepEqual(await s.invoke('search', { q: 'x' }), { q: 'x' }); s.dispose(); }); +test('update rejects a missing required precondition and keeps the prior binding', async () => { + const guarded = { ...contract, preconditions: ['allowed'] }; + let originalCalls = 0; + let replacementCalls = 0; + const s = createScope( + [ + { + contract: guarded, + handler: (x) => { + originalCalls++; + return x; + }, + preconditions: { allowed: () => true }, + }, + ], + { adapter: unsupported }, + ); + assert.throws( + () => + s.update([ + { + contract: guarded, + handler: (x) => { + replacementCalls++; + return x; + }, + }, + ]), + { code: 'PRECONDITION' }, + ); + assert.deepEqual(await s.invoke('search', { q: 'still authorized' }), { q: 'still authorized' }); + assert.equal(originalCalls, 1); + assert.equal(replacementCalls, 0); + s.dispose(); +}); test('unsupported browser is a status, not a fake native API', async () => { const s = createScope([binding((x) => x)], { adapter: unsupported }); assert.equal((await s.ready).registration, 'unsupported');