Skip to content

refactor(storage): drop the retired @hasna/cloud dependency - #19

Merged
andrei-hasna merged 2 commits into
mainfrom
chore/remove-hasna-cloud
Jul 27, 2026
Merged

refactor(storage): drop the retired @hasna/cloud dependency#19
andrei-hasna merged 2 commits into
mainfrom
chore/remove-hasna-cloud

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What

Removes the retired @hasna/cloud dependency. src/storage.ts dynamically loaded
both of its adapters behind the config-driven storage.cloud.backend switch, so both
halves are replaced in-repo rather than dropped:

file replaces notes
src/db/sqlite-adapter.ts SqliteAdapter 43 lines over bun:sqlite
src/db/pg-adapter.ts PgAdapterAsync ~90 lines over pg

pg becomes a direct dependency. It was already installed transitively and already
bundled into dist through the retired package, so the resolved tree only shrinks.
Lockfile net effect: two packages removed (@hasna/cloud, @hasna/events — the
latter transitive-only, and referenced nowhere in src, tests or docs) and one
added
(@types/pg@8.20.0, a dev type package).

The public storage.cloud config surface is unchangedbackend: "sqlite" | "postgres"
both still work, no config migration, no breaking change for consumers. Both adapters stay
behind lazy await import(...) so the Postgres driver is only loaded when a Postgres
ledger is actually configured.

Two details that are load-bearing

PRAGMA foreign_keys=ON is preserved. It is per-connection in SQLite and defaults to
OFF, so dropping it raises no error anywhere — it silently turns every ON DELETE CASCADE
and every foreign key constraint into a no-op. tests/sqlite-adapter.test.ts proves the
pragma is set, proves a cascade actually deletes child rows, and proves a dangling
reference is rejected. Deleting the pragma line makes 3 of those tests fail, so the guard
is not vacuous.

The ? -> $1, $2, ... placeholder rewrite is preserved. Postgres has no ?
placeholder form, and the ledger INSERT binds 14 columns; an off-by-one would bind the
wrong value to every column after the mistake. That exact 14-binding statement is asserted
in tests/pg-adapter.test.ts.

The rewrite is deliberately a placeholder rewrite only, not the retired package's full
dialect translator. Every statement reaching the adapter is written in src/storage.ts
(the DDL, two CREATE INDEX, one INSERT, one SELECT) and placeholders are the only
construct among them that Postgres spells differently. Notably REAL is left alone —
the retired adapter's exec() path did not rewrite it either, so this stays like-for-like
and existing deployed tables are unaffected.

lastInsertRowid exists only to mirror bun:sqlite's RunResult. Postgres has no rowid,
so resolveLastInsertRowid reports 0 unless a statement returns a numeric id. The
ledger declares id TEXT, so a future RETURNING id would otherwise have pushed a uuid
string through a number | bigint annotation with no error anywhere.

The published artifact was carrying the retired package

This is the part that is not visible from the manifest. On main, bun build inlined
the entire retired package — sync daemon, sync push/pull, dialect translator and all —
into all four dist entry points
, reachable from one dynamic import. Every entry point
drops ~0.25 MB:

entry point before after
dist/index.js 0.88 MB 0.63 MB
dist/cli/index.js 0.89 MB 0.64 MB
dist/mcp/index.js 1.18 MB 0.93 MB
dist/serve.js 0.69 MB 0.44 MB

Retired sync symbols (syncPush, syncPull, translateDdl, sqliteToPostgres,
runSync, scheduled-sync, HASNA_CLOUD*) now match 0 times in the bundle and 0
times in the packed tarball, and the node_modules/@hasna/cloud bundle banner is gone
from all four entry points (it was present in all four before).

The boundary guard now covers what it claimed to

tests/no-cloud-boundary.test.ts originally scanned src/**/*.ts only, which let a
literal import "@hasna/cloud" from scripts/ or tests/ pass, and it never looked at
build output — the exact failure mode the section above is about. It now:

  • scans every file git tracks (git ls-files, this guard file excepted), and throws
    rather than scanning nothing if git ls-files fails;
  • scans dist when a build is present, permitting exactly one occurrence: the
    FORBIDDEN_SHARED_CLOUD_RUNTIMES declaration bun build inlines from
    @hasna/contracts. Anything else is a real bundled edge and fails, with file:line;
  • runs after bun run build in bun run check, prepublishOnly and CI, so the
    built-output assertion executes instead of skipping;
  • documents its one blind spot in the docstring: byte matching sees literals only, so a
    specifier assembled at runtime is invisible to it (as it is to every byte scanner).

Non-vacuity, measured: a literal import staged under scripts/ fails it (exit 1), the same
under tests/ fails it (exit 1), an injected dist edge fails it with dist/serve.js:13379
(exit 1), and with no build present the built-output assertion reports skip, not pass.

Verification

All exit codes measured unpiped (cmd >log 2>&1; echo $?).

check result
bun install --frozen-lockfile exit 0
bun install (non-frozen) exit 0, zero bun.lock drift
bun run typecheck exit 0
bun run build exit 0
bun test exit 0 — 236 pass, 0 fail, 714 expect() across 20 files (main is 210 pass / 17 files)
bun run check (typecheck + build + test) exit 0
bun run contracts:validate exit 0
bun dist/cli/index.js validate --config gateway.config.production-cloud.example.json exit 0
secrets workspace scan exit 0 — 10 findings, all pre-existing files outside this diff

Structural proof that nothing survives:

  • package.json — 0 occurrences
  • bun.lock — 0 occurrences (@hasna/cloud, open-cloud)
  • tracked tree, all files — 0 occurrences of @hasna/cloud / open-cloud / hasna-cloud
  • node_modules/@hasna/ after a from-scratch install — only contracts (a plain
    bun install did not prune the stale directory, so this was re-verified from an
    empty node_modules); the only cloud-matching directory left is pg-cloudflare,
    an optional dep of pg
  • dist and the npm pack tarball — one occurrence each in dist/index.js:17728 and
    dist/cli/index.js:16081, both the line var FORBIDDEN_SHARED_CLOUD_RUNTIMES = [...]
    vendored from @hasna/contracts. Zero import or require edges.

Note on no-cloud-scan (measured with @hasna/contracts@0.8.1)

target result
this branch, tracked tree (git archive HEAD) exit 0, ok hasna.no_cloud_evidence_pack.v1
main, tracked tree, same tool and method exit 1, 4 findings — 2 critical (package_manifest package.json, lockfile bun.lock) + 2 high (package_manifest, source_import src/storage.ts)
this branch, built working tree exit 1, 4 high on dist/index.js + dist/cli/index.js
this branch, packed tarball (npm pack) exit 1, the same 4 findings escalated to critical as packed_artifact

The pass on the tracked tree is therefore discriminating, not vacuous — same tool, same
method, opposite result on main.

The two exit-1 rows are a false positive, and a new class beyond the comment/guard-test
one fixed in contracts #32: the scanner reads a repo's own build output, and
@hasna/contracts is itself bundled into dist, so its denylist constant
FORBIDDEN_SHARED_CLOUD_RUNTIMES = ["@hasna/cloud", "open-cloud"] is inlined verbatim.
That string exists nowhere in this repo's source (git grep FORBIDDEN_SHARED_CLOUD over
tracked files finds only the guard test that whitelists it). Any repo that bundles
@hasna/contracts will hit this, and the packed-artifact form escalates it to critical,
which is the form a release lane would run.

Consequences, now recorded in docs/publishing-and-release.md so a future reader does not
have to rediscover them: do not gate the release on no-cloud-scan against dist or
against an npm pack tarball until the scanner skips build output or exempts that
declaration — run it against the tracked tree, and rely on
tests/no-cloud-boundary.test.ts for built output. And do not wave off a built-output
finding as "just the vendored constant" without reading it; that excuse is true for exactly
one line and false for everything else, which is why the test encodes the distinction
instead of leaving it to eye.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

The shared cloud runtime is retired, so the usage-ledger backends it
supplied now live in the repo. src/storage.ts loaded both of its
adapters behind the config-driven storage.cloud.backend switch, so both
halves are replaced rather than removed:

- src/db/sqlite-adapter.ts wraps bun:sqlite and keeps both pragmas the
  retired adapter set. foreign_keys is per-connection and defaults to
  OFF in SQLite, so losing it would not raise an error anywhere -- it
  would silently turn every ON DELETE CASCADE into a no-op.
- src/db/pg-adapter.ts wraps pg and rewrites the ledger statements'
  `?` placeholders into Postgres' numbered form, normalizes bindings,
  and keeps the "encrypt without verifying" TLS handling for
  sslmode=require.

pg becomes a direct dependency; it was already installed and already
bundled into dist through the retired package, so the resolved tree only
shrinks. The public storage.cloud config surface is unchanged, and both
adapters stay behind lazy imports so the postgres driver is only loaded
when a postgres ledger is configured.

Removing the dependency also removes it from the published artifact:
bun build inlined the whole retired package -- sync daemon included --
into all four dist entry points, each of which now drops ~0.25 MB.

Verified: bun run check exits 0 with 232 tests passing (210 before), a
--frozen-lockfile install plus typecheck/test/build reproduce green from
a pristine tree, and no-cloud-scan on the tracked tree goes from exit 1
with two critical findings to exit 0.
…t output

The guard scanned src/**/*.ts only, so a literal import of the retired shared
cloud runtime from scripts/ or tests/ passed it, and it never looked at build
output — the one place this repo actually was shipping the retired package from.

- scan every file git tracks (git ls-files, this guard file excepted) instead of
  src/, and fail loudly if git ls-files errors rather than scanning nothing
- scan dist when a build is present, permitting exactly one occurrence: the
  FORBIDDEN_SHARED_CLOUD_RUNTIMES declaration that bun build inlines from
  @hasna/contracts. Any other occurrence is a real bundled edge and fails
- build before test in bun run check, prepublishOnly and CI so that assertion
  runs instead of skipping
- note in the guard docstring that byte matching sees literals only, so a
  specifier assembled at runtime is a known blind spot

Also in this commit:

- pg-adapter: extract resolveLastInsertRowid and refuse a non-numeric id. The
  ledger declares id TEXT, so a future RETURNING id would have handed a uuid
  string through a number | bigint annotation with no error. Covered by three
  new cases in tests/pg-adapter.test.ts
- docs/publishing-and-release.md: the no-cloud boundary check is implemented, so
  stop describing it as pending; record why the external artifact scan cannot be
  a hard release gate yet (it reports the inlined denylist constant as critical
  on a packed tarball) and why a built-output finding must still be read rather
  than waved off

bun run typecheck, bun run build, bun test all exit 0 (236 pass, 0 fail).
Guard non-vacuity: a literal import staged under scripts/ fails it, the same
under tests/ fails it, and an injected dist edge fails it with file:line.
@andrei-hasna
andrei-hasna merged commit dce573d into main Jul 27, 2026
1 check passed
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

Merged as dce573d (squash) after the review findings were fixed on the branch in 822ad1a:

  • Guard bypass (blocker for merge as-was)tests/no-cloud-boundary.test.ts scanned src/**/*.ts only, so a literal import from scripts/ or tests/ passed it, and it never inspected build output. It now scans every file git ls-files reports (this guard excepted, and it throws rather than scanning nothing if git ls-files fails) and scans dist when a build is present, permitting exactly one occurrence: the FORBIDDEN_SHARED_CLOUD_RUNTIMES declaration bun build inlines from @hasna/contracts. bun run check, prepublishOnly and CI now build before testing so that assertion runs — confirmed in the CI log for 822ad1a: all 5 guard tests (pass), 0 skips.
  • Non-vacuity, measured unpiped — literal import staged under scripts/: exit 1. Same under tests/: exit 1. Injected dist edge: exit 1, reported as dist/serve.js:13379. No build present: the built-output assertion reports skip, not pass.
  • Latent type lieresolveLastInsertRowid now refuses a non-numeric id. The ledger declares id TEXT, so a future RETURNING id would have pushed a uuid string through a number | bigint annotation with no error. Three new cases cover it.
  • Computed specifiers — documented as the guard's blind spot in its docstring rather than papered over; byte matching sees literals only.
  • PR body corrections — lockfile net effect is 2 removed / 1 added (@types/pg@8.20.0), main's tracked tree is exit 1 with 4 findings (2 critical + 2 high) not 2 critical, and the npm pack escalation to critical is now disclosed.
  • Scanner false positive — not fixable here; filed as no-cloud-scan reports a consumer's bundled copy of this package's own denylist as critical (packed_artifact) contracts#34 with the reproduction. docs/publishing-and-release.md records why no-cloud-scan must not hard-gate dist or a packed tarball yet, and warns against waving off a built-output finding as "just the vendored constant".
  • Left alone deliberatelypg stays in dependencies (supports unbundled consumption; net install cost is still lower than before).

Verification on the merged main (dce573d), exit codes unpiped: bun install --frozen-lockfile 0, bun run check (typecheck + build + test) 0 with 236 pass / 0 fail, contracts@0.8.1 no-cloud-scan on the tracked tree 0 (ok hasna.no_cloud_evidence_pack.v1). No open PRs remain on this repo, so nothing retargeted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant