Skip to content

Add @weftsh/sdk: the TypeScript SDK for Weft - #1

Merged
hkd987 merged 4 commits into
mainfrom
claude/weft-typescript-sdk-qp6bkd
Sep 25, 2026
Merged

hkd987 merged 4 commits into
mainfrom
claude/weft-typescript-sdk-qp6bkd

Conversation

@hkd987

@hkd987 hkd987 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

What

The first release (0.1.0) of @weftsh/sdk, a small TypeScript client for the Weft Repos API. It's shaped after the code.storage SDK: a Weft client, a Repo handle, and one method per action.

const weft = new Weft({ token: process.env.WEFT_TOKEN!, org: 'acme' });
const repo = await weft.createRepo();
await repo.createCommit({ message: 'step 1' }).put('src/app.ts', src).delete('old.txt').send();
await repo.readFile('src/app.ts');
await repo.getRemoteURL(); // https://x:weft_…@api.weft.sh/acme/….git
  • Repositories: create (the name is generated if you don't give one), find, get a handle without a request, list and iterate, batch create/delete, update, delete, fork, mirrors.
  • Commits: a builder (createCommit().put().delete().send()). Strings are sent as put and bytes as put_base64. expectedParent covers the server's three cases: omitted, a SHA, or null (the branch must not exist yet). A 409 throws WeftConflictError, and its currentTip is read from either conflict shape the server returns (current_tip for commits, current for reset/revert).
  • Reads: files, with ETag round trips and the X-Weft-* headers; trees; flat file lists; paginated history, optionally filtered by path; diffs.
  • Refs and undo: branches, tags, reset, revert.
  • Other: tokens, webhooks, bundle export.
  • getRemoteURL(): each call creates a short-lived token bound to that one repository and puts it in the URL. A sandbox can clone and push without the client's own token. If the mint is refused (400/403/404), it throws a WeftError that names the scopes needed rather than passing on the server's masked 404.
  • verifyWebhook(): checks X-Weft-Signature-256 using Web Crypto.
  • Packaging: no dependencies, ESM and CommonJS with type definitions, Node 20+ (also runs on Bun, Deno and edge runtimes).
  • Docs and CI: README with a Quickstart, a full guide and an API reference; CHANGELOG; examples/quickstart.mts and examples/agent-session.ts; and a CI workflow that runs typecheck, tests and build on Node 20/22/24 and checks that both module formats load.

readFile and getFile return null only for a missing path or revision. A missing (or invisible) repository throws a 404, because the server answers that case with a bare not found rather than a JSON error. Without this, a typo in a repository name looked like a missing file.

Quickstart

The README opens with a Quickstart:

  1. Get a token.
  2. Install.
  3. Run examples/quickstart.mts, which creates a repository, commits, reads the file back and clones it with the real git CLI.
  4. Compare with the expected output.

The README embeds the script verbatim. A unit test fails if the two differ, and the e2e suite runs the script as a subprocess against a live server.

Writing it surfaced a wrong claim. The README said a personal token "whose owner can write" can call getRemoteURL(), but minting also needs org:read. I checked with real personal tokens: org:read + repo:write runs the whole quickstart; repo:write alone fails at the clone step. The README is corrected, and that failure now explains itself.

Testing

  • Unit tests (32) pin what goes on the wire, using a fake fetch. They also check that the README quickstart matches the example file; a one-character drift turns that test red.
  • End-to-end tests (18) run against a real Weft server built from stratum-core. They are skipped unless WEFT_E2E_URL, WEFT_E2E_TOKEN and WEFT_E2E_ORG are set. Among other things they:
    • clone and push with the real git CLI through getRemoteURL, and run git fsck --full --strict on the clone;
    • check that a read-only URL can't push, and that a repo-scoped token can't reach a sibling repository;
    • verify a webhook delivery the server itself signed;
    • check that a missing repository throws while a missing branch reads as null;
    • run the README quickstart as written.
  • Deliberately breaking the expected_parent mapping turned 4 end-to-end tests and 2 unit tests red.

npm run check (typecheck, tests, build) passes. All 50 tests pass against a local server, and with no server set, 32 pass and 18 skip.

Before publishing

  • The package isn't on npm yet, and the @weftsh npm scope needs to belong to us.
  • The license is MIT, matching code.storage. Change it if that's not what we want.
  • The weft.sh docs and marketing for the SDKs are in hkd987/stratum-core#106.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13

A small, dependency-free client for the Weft Repos API, shaped after the
code.storage SDK: a `Weft` client, a `Repo` handle, and one method per
thing you want to do.

- Repositories: create (with a generated name by default), find, hydrate
  without a request, list and iterate, batch create/delete, update,
  delete, fork; mirrors.
- Commits through a builder (`createCommit().put().delete().send()`):
  strings go as `put`, bytes as `put_base64`, `expectedParent` maps to
  the server's three cases (omitted / SHA / null), and a 409 surfaces as
  `WeftConflictError` with `currentTip` from either conflict shape the
  server uses (`current_tip` on commits, `current` on reset/revert).
- Reads: files with ETag round trips and the X-Weft-* headers, trees,
  flat listings, paginated and path-filtered history, diffs.
- Refs, reset/revert, tokens, webhooks, bundle export.
- `getRemoteURL()` mints a short-lived token bound to one repository and
  embeds it, so a sandbox can clone and push without the client's token.
- `verifyWebhook()` on Web Crypto, for X-Weft-Signature-256.

Tested two ways. Unit tests pin what goes on the wire. The e2e suite
runs against a real Weft server: it clones and pushes with the real git
CLI through getRemoteURL, runs `git fsck --full --strict`, proves a
read-only URL cannot push and a repo-scoped credential cannot reach a
sibling repository, and verifies a webhook delivery the server itself
signed. Breaking the `expected_parent` mapping turns four e2e cases red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13
… null

getFile and readFile mapped every 404 to `null`. But the server answers
three different misses with a 404: a path not at that revision and an
unknown revision both as JSON `{ "error": … }`, and a repository that
does not exist — or that the caller cannot see — as a bare `not found`.
So `weft.repo('tpyo').readFile('README.md')` resolved to `null`, which
reads as "the file is not there" when the truth is "you named the wrong
repository", and an agent would go on to write that file into a
repository that does not exist.

Only a 404 with a JSON error body is now treated as a miss inside the
repository; anything else is rethrown. Pinned by a unit test on the
wire shape and by an e2e case against the real server, which also
checks that an unknown branch still reads as null.

Also says in the types and README that `branch` defaults to "main"
rather than the repository's default branch — the server's default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13
CI failed with "Weft: `token` must be a non-empty string" on every
Node version. `describe.skipIf` skips the tests, but vitest still runs
the describe body to collect them, and the body built a `Weft` from the
unset WEFT_E2E_TOKEN — so the constructor's own validation threw and
failed the file. The local "skips cleanly" check only unset the URL, so
the token was still in the environment and the failure never showed.

The client is now built only when the suite is enabled. Checked with all
three variables unset: 26 passed, 17 skipped, and every CI step passes
from a fresh `npm ci`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13
…oteURL

The README opened with a snippet and went straight into reference. It
now has a Quickstart: get a token (with the scopes it actually needs),
install, save and run examples/quickstart.mts, see the output. The
script creates a repository, commits over HTTP, reads the file back and
clones it with the real git CLI through getRemoteURL().

The README embeds the script verbatim between markers; a unit test fails
if the two differ, and the e2e suite runs the script as a subprocess
against a live server, importing '@weftsh/sdk' by name from the build.
tsconfig maps '@weftsh/sdk' to src so the example typechecks before a
build.

Two things the quickstart surfaced:

- The README said getRemoteURL() needs "a personal token whose owner can
  write to the repository". Not enough: minting also needs org:read on
  the calling token. Checked against the server with real personal
  tokens: org:read + repo:write runs the whole quickstart; repo:write
  alone fails at step 4.
- That failure surfaced as "POST /v1/orgs/acme/tokens answered 404",
  because the server masks what the caller cannot see. getRemoteURL now
  rethrows a 400/403/404 from the mint as a WeftError that names the
  scopes needed, keeping the status, body and the original as cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13
@hkd987
hkd987 merged commit 76669b0 into main Sep 25, 2026
3 checks passed
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.

2 participants