From cb81923a9e16119cd3d5f4eb4bf924b60ebeec3f Mon Sep 17 00:00:00 2001
From: Michiel de Jong
Date: Wed, 9 Sep 2026 14:44:26 +0200
Subject: [PATCH 1/3] Run LocalThought integrations entirely in the browser
---
.dagger/src/index.ts | 20 +-
Cargo.lock | 6 +-
Cargo.toml | 2 +-
TESTING_COVERAGE.md | 19 +-
.../chunks/PluginRuns/ConnectLocalThought.tsx | 69 ++-
.../chunks/PluginRuns/LocalThoughtCatalog.tsx | 24 +-
.../PluginRuns/installationResources.test.ts | 34 ++
.../PluginRuns/installationResources.ts | 67 ++-
.../chunks/PluginRuns/localImportVerdict.ts | 49 ++
.../src/chunks/PluginRuns/localThought.ts | 82 ++-
browser/data-browser/src/locales/de.po | 8 +
browser/data-browser/src/locales/en.po | 8 +
browser/data-browser/src/locales/es.po | 8 +
browser/data-browser/src/locales/fr.po | 8 +
browser/e2e/tests/plugins.spec.ts | 25 +-
integrations/localthought/README.md | 132 ++---
integrations/localthought/browser-smoke.mjs | 63 +++
integrations/localthought/browser.test.ts | 126 +++++
integrations/localthought/browser.ts | 278 ++++++++++
integrations/localthought/mock-proxy.mjs | 6 +
.../localthought/syncables/Cargo.toml | 47 ++
integrations/localthought/syncables/LICENSE | 201 +++++++
integrations/localthought/syncables/README.md | 209 +++++++
.../localthought/syncables/UPSTREAM.md | 1 +
.../syncables/src/client/client.rs | 369 +++++++++++++
.../localthought/syncables/src/client/mod.rs | 5 +
.../syncables/src/client/storage.rs | 84 +++
.../localthought/syncables/src/error.rs | 95 ++++
.../syncables/src/fake_data/generate.rs | 83 +++
.../syncables/src/fake_data/mod.rs | 3 +
.../localthought/syncables/src/lib.rs | 81 +++
.../syncables/src/mock_server/mod.rs | 6 +
.../syncables/src/mock_server/server.rs | 91 +++
.../syncables/src/mock_server/store.rs | 89 +++
.../syncables/src/openapi/load.rs | 91 +++
.../localthought/syncables/src/openapi/mod.rs | 12 +
.../syncables/src/openapi/overlay.rs | 209 +++++++
.../syncables/src/openapi/resolve_refs.rs | 92 ++++
.../syncables/src/openapi/types.rs | 250 +++++++++
.../syncables/src/pagination/autodetect.rs | 169 ++++++
.../syncables/src/pagination/items.rs | 91 +++
.../syncables/src/pagination/mod.rs | 20 +
.../src/pagination/request_builder.rs | 144 +++++
.../src/pagination/response_parser.rs | 237 ++++++++
.../syncables/src/pagination/types.rs | 255 +++++++++
.../syncables/src/pagination/validate.rs | 112 ++++
.../syncables/src/resources/discover.rs | 106 ++++
.../syncables/src/resources/mod.rs | 3 +
.../localthought/syncables/src/routing/mod.rs | 3 +
.../syncables/src/routing/router.rs | 59 ++
.../localthought/syncables/src/sync/client.rs | 520 ++++++++++++++++++
.../syncables/src/sync/constants.rs | 129 +++++
.../syncables/src/sync/credentials.rs | 70 +++
.../localthought/syncables/src/sync/mod.rs | 24 +
.../syncables/src/sync/ontology.rs | 273 +++++++++
.../syncables/src/sync/resource_model.rs | 426 ++++++++++++++
.../syncables/src/sync/storage.rs | 221 ++++++++
integrations/localthought/tsconfig.json | 2 +-
integrations/localthought/wasm-smoke.mjs | 72 +++
planning/api-plugins.md | 9 +
server/Cargo.toml | 1 -
server/src/handlers/integration_proxy.rs | 316 -----------
server/src/handlers/integration_proxy_sync.rs | 515 -----------------
server/src/handlers/mod.rs | 4 -
server/src/routes.rs | 20 -
wasm/Cargo.toml | 4 +
wasm/src/integrations.rs | 260 +++++++++
wasm/src/lib.rs | 2 +
68 files changed, 6097 insertions(+), 1022 deletions(-)
create mode 100644 browser/data-browser/src/chunks/PluginRuns/localImportVerdict.ts
create mode 100644 integrations/localthought/browser-smoke.mjs
create mode 100644 integrations/localthought/browser.test.ts
create mode 100644 integrations/localthought/browser.ts
create mode 100644 integrations/localthought/syncables/Cargo.toml
create mode 100644 integrations/localthought/syncables/LICENSE
create mode 100644 integrations/localthought/syncables/README.md
create mode 100644 integrations/localthought/syncables/UPSTREAM.md
create mode 100644 integrations/localthought/syncables/src/client/client.rs
create mode 100644 integrations/localthought/syncables/src/client/mod.rs
create mode 100644 integrations/localthought/syncables/src/client/storage.rs
create mode 100644 integrations/localthought/syncables/src/error.rs
create mode 100644 integrations/localthought/syncables/src/fake_data/generate.rs
create mode 100644 integrations/localthought/syncables/src/fake_data/mod.rs
create mode 100644 integrations/localthought/syncables/src/lib.rs
create mode 100644 integrations/localthought/syncables/src/mock_server/mod.rs
create mode 100644 integrations/localthought/syncables/src/mock_server/server.rs
create mode 100644 integrations/localthought/syncables/src/mock_server/store.rs
create mode 100644 integrations/localthought/syncables/src/openapi/load.rs
create mode 100644 integrations/localthought/syncables/src/openapi/mod.rs
create mode 100644 integrations/localthought/syncables/src/openapi/overlay.rs
create mode 100644 integrations/localthought/syncables/src/openapi/resolve_refs.rs
create mode 100644 integrations/localthought/syncables/src/openapi/types.rs
create mode 100644 integrations/localthought/syncables/src/pagination/autodetect.rs
create mode 100644 integrations/localthought/syncables/src/pagination/items.rs
create mode 100644 integrations/localthought/syncables/src/pagination/mod.rs
create mode 100644 integrations/localthought/syncables/src/pagination/request_builder.rs
create mode 100644 integrations/localthought/syncables/src/pagination/response_parser.rs
create mode 100644 integrations/localthought/syncables/src/pagination/types.rs
create mode 100644 integrations/localthought/syncables/src/pagination/validate.rs
create mode 100644 integrations/localthought/syncables/src/resources/discover.rs
create mode 100644 integrations/localthought/syncables/src/resources/mod.rs
create mode 100644 integrations/localthought/syncables/src/routing/mod.rs
create mode 100644 integrations/localthought/syncables/src/routing/router.rs
create mode 100644 integrations/localthought/syncables/src/sync/client.rs
create mode 100644 integrations/localthought/syncables/src/sync/constants.rs
create mode 100644 integrations/localthought/syncables/src/sync/credentials.rs
create mode 100644 integrations/localthought/syncables/src/sync/mod.rs
create mode 100644 integrations/localthought/syncables/src/sync/ontology.rs
create mode 100644 integrations/localthought/syncables/src/sync/resource_model.rs
create mode 100644 integrations/localthought/syncables/src/sync/storage.rs
create mode 100644 integrations/localthought/wasm-smoke.mjs
delete mode 100644 server/src/handlers/integration_proxy.rs
delete mode 100644 server/src/handlers/integration_proxy_sync.rs
create mode 100644 wasm/src/integrations.rs
diff --git a/.dagger/src/index.ts b/.dagger/src/index.ts
index c177cf9aef..dc4e0a4b36 100644
--- a/.dagger/src/index.ts
+++ b/.dagger/src/index.ts
@@ -636,6 +636,10 @@ export class AtomicServer {
this.source.directory('plugin-runtime'),
)
.withDirectory('/code/wasm', this.source.directory('wasm'))
+ .withDirectory(
+ '/code/integrations/localthought/syncables',
+ this.source.directory('integrations/localthought/syncables'),
+ )
.withDirectory('/code/server', this.source.directory('server'))
.withDirectory('/code/cli', this.source.directory('cli'))
.withDirectory('/code/desktop', this.source.directory('desktop'))
@@ -713,6 +717,10 @@ export class AtomicServer {
.withDirectory('/code/cli', this.source.directory('cli'))
.withDirectory('/code/desktop', this.source.directory('desktop'))
.withDirectory('/code/wasm', this.source.directory('wasm'))
+ .withDirectory(
+ '/code/integrations/localthought/syncables',
+ this.source.directory('integrations/localthought/syncables'),
+ )
.withDirectory(
'/code/plugin-examples',
this.source.directory('plugin-examples'),
@@ -1127,7 +1135,9 @@ export class AtomicServer {
// Surfaces /app/dev-drive and /app/prunetests in the production
// build the e2e tests run against. See `devRoutesEnabled()` in
// data-browser/src/config.ts.
- buildContainer = buildContainer.withEnvVariable('VITE_E2E', 'true');
+ buildContainer = buildContainer
+ .withEnvVariable('VITE_E2E', 'true')
+ .withEnvVariable('VITE_INTEGRATION_PROXY_URL', 'http://127.0.0.1:19090');
}
return buildContainer.withExec(['pnpm', 'run', 'build']);
@@ -1182,6 +1192,10 @@ export class AtomicServer {
.withDirectory('/code/cli', source.directory('cli'))
.withDirectory('/code/desktop', source.directory('desktop'))
.withDirectory('/code/wasm', source.directory('wasm'))
+ .withDirectory(
+ '/code/integrations/localthought/syncables',
+ source.directory('integrations/localthought/syncables'),
+ )
.withDirectory(
'/code/plugin-examples',
source.directory('plugin-examples'),
@@ -1380,6 +1394,10 @@ export class AtomicServer {
.withDirectory('/code/cli', source.directory('cli'))
.withDirectory('/code/desktop', source.directory('desktop'))
.withDirectory('/code/wasm', source.directory('wasm'))
+ .withDirectory(
+ '/code/integrations/localthought/syncables',
+ source.directory('integrations/localthought/syncables'),
+ )
.withDirectory(
'/code/plugin-examples',
source.directory('plugin-examples'),
diff --git a/Cargo.lock b/Cargo.lock
index 7f5b7ea106..e675c1e34f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1282,7 +1282,6 @@ dependencies = [
"sha1 0.11.0",
"simple-server-timing-header",
"static-files 0.3.1",
- "syncables",
"tempfile",
"text-splitter",
"tokio",
@@ -1345,14 +1344,17 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
name = "atomic-wasm"
version = "0.1.0"
dependencies = [
+ "async-trait",
"atomic_lib",
"blake3",
+ "chrono",
"console_error_panic_hook",
"getrandom 0.3.4",
"js-sys",
"serde",
"serde-wasm-bindgen",
"serde_json",
+ "syncables",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
@@ -13442,7 +13444,6 @@ dependencies = [
[[package]]
name = "syncables"
version = "0.1.0"
-source = "git+https://github.com/localthought/syncables-rs?rev=d48e4d9bad3ed9ec826d1c2040e989171701965d#d48e4d9bad3ed9ec826d1c2040e989171701965d"
dependencies = [
"async-trait",
"httpdate",
@@ -13453,7 +13454,6 @@ dependencies = [
"serde_yaml_ng",
"thiserror 2.0.18",
"tokio",
- "uuid",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index a575b5bc79..740f704af9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,7 @@ members = [
"plugin-runtime",
"tools/cargo-bin",
]
-exclude = ["flutter/rust"]
+exclude = ["flutter/rust", "integrations/localthought/syncables"]
# Debuginfo dominates target/ size: with ~1460 deps (tauri, actix, iroh) the
# default `debug = true` produces a multi-GB tree per build flavor, and this
diff --git a/TESTING_COVERAGE.md b/TESTING_COVERAGE.md
index 6e3c8f1b5a..dbae936d47 100644
--- a/TESTING_COVERAGE.md
+++ b/TESTING_COVERAGE.md
@@ -1,5 +1,18 @@
# Testing coverage map
+LocalThought browser migration: `integrations/localthought/browser.test.ts`
+covers tenant HMAC, actor/drive ownership, rotation before dispatch, pagination,
+uncertain-response refusal and cross-origin pagination refusal. The real generated
+WASM bundle is exercised by `wasm-smoke.mjs` for pagination, typed ontology,
+timestamps and provider failures. `browser-smoke.mjs` exercises the complete
+mock consent/import/review/OPFS/reload journey with AtomicServer unavailable
+(verified locally). Local installation/schema lookup tests reject missing or
+incomplete local databases rather than inferring permission to create duplicates.
+The companion Syncables branch has 142 passing native tests and a wasm32 build;
+the companion proxy branch has 39 passing tests including CORS preflight and
+exposed headers. Live OAuth on the browser path still requires deployment of
+the companion proxy CORS change and is not yet verified.
+
What is tested, at which layer, and — the part that matters — **what is not**.
This exists because the protocol is far better tested than the glue around it,
@@ -65,12 +78,12 @@ return to the same drive, rotating connection codes, two-page Syncables fetch,
review/apply, and five displayed records with integer/boolean/float/timestamp
properties. Dagger starts the mock for E2E; local runs opt in with
`ATOMIC_MOCK_INTEGRATION_PROXY=1` and the README configuration.
-`integration_proxy` Rust tests cover actor/drive binding, tenant HMAC,
+`browser.test.ts` and the real WASM smoke cover actor/drive binding, tenant HMAC,
Syncables pagination/ontology and duplicate-page refusal. The mock's Node test
covers invalid tenant proofs and replayed/rotated codes. The mapping tests cover
typed proposals, missing identities, repeat imports, local edits and duplicates.
-Live catalog OAD endpoints currently return 404 (integration-proxy #25), so live
-OAuth and GitHub data fetching are not yet certified.
+The historical server path was live-verified for GitHub and Google Calendar.
+The new browser path awaits deployment of the companion proxy CORS change.
Run it against a production build to catch missing translation catalog entries:
Vite dev extracts them automatically and can hide blank production labels.
The GitHub setup flow also covers opting into assistant-led automation creation:
diff --git a/browser/data-browser/src/chunks/PluginRuns/ConnectLocalThought.tsx b/browser/data-browser/src/chunks/PluginRuns/ConnectLocalThought.tsx
index bb33f0ea6e..4be4f39df2 100644
--- a/browser/data-browser/src/chunks/PluginRuns/ConnectLocalThought.tsx
+++ b/browser/data-browser/src/chunks/PluginRuns/ConnectLocalThought.tsx
@@ -3,7 +3,7 @@ import {
core,
dataBrowser,
ensureSchema,
- executeServerPlugin,
+ pluginSchema,
useStore,
type Resource,
} from '@tomic/react';
@@ -12,10 +12,13 @@ import { Column } from '@components/Row';
import Field from '@components/forms/Field';
import { Input, ErrMessage } from '@components/forms/InputStyles';
import { AtomicLink } from '@components/AtomicLink';
-import { pluginClassesFor } from './runScript';
-import { ensureInstallationResource } from './installationResources';
+import {
+ localSchemaStore,
+ ensureLocalInstallationResource as ensureInstallationResource,
+} from './installationResources';
import { RunPluginDialog } from './RunPluginDialog';
import {
+ browserIntegrations,
connectionKey,
platformName,
proxyRequest,
@@ -27,6 +30,7 @@ import {
type FetchedPlatform,
} from '../../../../../integrations/localthought/schema';
import type { Config } from '../../../../../integrations/localthought/plugin';
+import { localImportVerdict } from './localImportVerdict';
import source from '../../../../../integrations/localthought/plugin.js?raw';
export function ConnectLocalThought({
@@ -55,6 +59,7 @@ export function ConnectLocalThought({
start: new Date().toISOString().slice(0, 10),
end: new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10),
}));
+ const [tenantSecret, setTenantSecret] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [preview, setPreview] = useState<{
@@ -65,13 +70,10 @@ export function ConnectLocalThought({
const [tables, setTables] = useState([]);
useEffect(() => {
const controller = new AbortController();
- fetch(
- `${store.getServerUrl()}/integration-proxy/platform?platform=${encodeURIComponent(platform)}`,
- { signal: controller.signal },
- )
- .then(async response => {
- if (!response.ok) throw new Error(await response.text());
- const data = await response.json();
+ browserIntegrations()
+ .describe(platform)
+ .then(data => {
+ if (controller.signal.aborted) return;
setParameters(data.parameters);
setCollections(data.collections);
setConstants(
@@ -104,7 +106,12 @@ export function ConnectLocalThought({
const result = await proxyRequest<{ url: string; state: string }>(
store,
'start',
- { drive, platform, returnUrl: `${location.origin}/app/integrations` },
+ {
+ drive,
+ platform,
+ tenantSecret,
+ returnUrl: `${location.origin}/app/integrations`,
+ },
);
sessionStorage.setItem(
'localthought-pending',
@@ -131,7 +138,8 @@ export function ConnectLocalThought({
});
if (fetched.platform !== platform)
throw new Error('Imported platform did not match this connection');
- const terms = await pluginClassesFor(store, drive);
+ const schemaStore = localSchemaStore(store);
+ const terms = await ensureSchema(schemaStore, drive, pluginSchema());
const name = platformName(platform);
const identity = `localthought:${connection.connection}:${JSON.stringify(Object.entries(constants).sort())}`;
const resource = await ensureInstallationResource(store, drive, {
@@ -145,7 +153,7 @@ export function ConnectLocalThought({
},
});
const schema = await ensureSchema(
- store,
+ schemaStore,
drive,
platformSchema(platform, fetched.ontology.terms),
);
@@ -203,24 +211,13 @@ export function ConnectLocalThought({
localthought: { ...config, connection: connection.connection },
});
await resource.save();
- const result = await executeServerPlugin(store, {
- drive,
- plugin: resource.subject,
- source,
- input: {
- config: { ...config, records: fetched.records },
- trigger: {
- kind: 'manual',
- at: Date.now(),
- subject: resource.subject,
- },
- },
+ const verdict = await localImportVerdict(store, drive, {
+ ...config,
+ records: fetched.records,
});
- if (result.error || !result.verdict)
- throw new Error(result.error ?? 'Import returned no preview');
setPreview({
resource,
- verdict: result.verdict,
+ verdict,
tables: Object.values(destinations).map(d => d.table),
});
} catch (reason) {
@@ -236,7 +233,21 @@ export function ConnectLocalThought({
Connect your personal account through LocalThought, then return here to
preview an import.
-