feat(client): cloud-http transport resolver — self_hosted clients actually use the cloud (B2/B5)#16
Merged
Merged
Conversation
…s actually use the cloud (B2) The core B2 fix. Setting a client to cloud/self_hosted mode was a no-op: the CLI/MCP kept reading the local SQLite/db.json store because a DSN on the client does not switch the dataset a CLI reads. Add a shared client-flip mechanism in @hasna/contracts (exported at the root and at the ./client subpath) that every app's storage resolver can adopt: - resolveClientTransport(name, env): decides local vs cloud-http from the env flip contract. Transport is cloud-http IFF the resolved mode is cloud AND an API key is present; base URL defaults to https://<app>.hasna.xyz and /v1 is appended. If cloud is requested but the API key is missing/URL invalid, it returns local + misconfigured=true with a loud warning so callers hard-fail instead of silently serving wrong local data. - createHasnaHttpTransport(): authenticated fetch client for <base>/v1, sends the key as both x-api-key and Authorization: Bearer, JSON in/out, typed errors. - createClientTransport(): resolve + build in one call; throws on misconfig. Flip contract (env), per app <NAME>: HASNA_<NAME>_STORAGE_MODE | HASNA_<NAME>_MODE = cloud|self_hosted|local HASNA_<NAME>_API_URL (default https://<app>.hasna.xyz) HASNA_<NAME>_API_KEY (hasna_<app>_...) Rollback = unset the mode/url/key; local original is never touched. The transport routes ALL reads+writes through the app's HTTP API, so it covers the app's REAL dataset (db.json items, not just sqlite tables) — addressing B5: the data source is the server, whatever the server persists. Includes an end-to-end test that stands up a real loopback /v1 server plus a demo storage resolver using the shared code, proving: flip on => reads/writes hit the cloud server; flip off => local file; key is sent and enforced. Never logs the key.
…h retries + idempotency Adds createHasnaStorageClient + resolveStorageClient on top of the cloud-http transport: the resource CRUD interface every Hasna serve app exposes under /v1, which an app's storage resolver selects when mode=self_hosted and API_URL+API_KEY are set (else local). Transport gains per-call query, retries (exp backoff+jitter, idempotent methods + POST-with-Idempotency-Key), and caller-abort propagation. Errors surface as HasnaHttpError. Live conformance test round-trips CRUD against a real cloud app (key from Secrets Manager; skips offline). Exported via @hasna/contracts and ./client/storage. Version 0.5.0.
Contributor
Author
|
Adds the HTTP storage client on top of the cloud-http transport (commit ccc7f5f):
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The B2 core fix. Setting a client to
cloud/self_hostedwas a no-op — the CLI/MCP kept reading the local SQLite/db.jsonstore. A DSN on the client does not switch the dataset a CLI reads.Adds a shared client-flip mechanism in
@hasna/contracts(root +./clientsubpath) that every app's storage resolver adopts:resolveClientTransport(name, env)— decideslocalvscloud-http. Transport iscloud-httpIFF resolved mode iscloudAND an API key is present. Base URL defaults tohttps://<app>.hasna.xyz;/v1appended. If cloud is requested but the key is missing / URL invalid → returnslocal+misconfigured: truewith a loud warning (never silently serves wrong local data).createHasnaHttpTransport()— authenticated fetch client for<base>/v1; sends the key as bothx-api-keyandAuthorization: Bearer; typed errors; never logs the key.createClientTransport()— resolve + build in one call; throws on misconfig.The flip contract (env), per app
<NAME>HASNA_<NAME>_STORAGE_MODE/HASNA_<NAME>_MODEcloud|self_hosted|localHASNA_<NAME>_API_URLhttps://<app>.hasna.xyz)HASNA_<NAME>_API_KEYhasna_<app>_...Rollback = unset those; the local original is never touched.
B5
The transport routes ALL reads+writes through the app's HTTP API, so the data source is the server (whatever it persists —
db.jsonitems, not just sqlite tables).Proof
src/client/transport.test.tsstands up a real loopback/v1server + a demo storage resolver using the shared code and proves: flip on → reads/writes hit the cloud server (write lands in the server store, local file untouched); flip off → local file; key sent + enforced. 13/13 pass; full suite 156 pass / 0 fail; typecheck + build clean.Follow-on (not in this PR)
bun:sqliteviagetDatabase(); knowledge todb.json+ an older/api/v1/*contract) — each needs a small resolver swap to route through this transport.origin/main, which is behind npm0.4.2; publishing from this base would regress — reconcile main first).