# @nibling/sdk The browser SDK for nibling's platform layer (VISION.md D18 to D22). It reads and writes nibling records on atproto, and it never imports `@nibling/kernel`. File trees cross its API as `Map`, and the app adapts them to the kernel's filesystem. Every write takes a `Session`, a `Client` authenticated as a DID against that DID's PDS. Every read takes a DID or an AT-URI and reads publicly from the owner's PDS. ## Reading through the gateway `configure({ gateway: 'https://api.nibling.dev' })` sends every read to nibling's gateway (`packages/api`, D22) instead of each service: repo reads and blobs, identity, Constellation backlinks, Slingshot's `getRecordByUri`, and tangled imports through `/git/`. The gateway caches, and it names us to each service with a User-Agent, which a browser cannot set. - A gateway that does not answer, times out, or returns a 5xx gets one retry on the direct path for that call (`preferGateway` in `gateway.ts`). A 4xx is an answer. The direct path for a repo read resolves the PDS without the gateway. - Reads of the signed-in account's own repo send `x-nib-fresh: 1`, so the gateway skips its cache and a save shows up on the next read. `freshFor()` in `identity.ts` makes that call. `auth.ts` sets `config.viewer` on sign-in and clears it on sign-out. - Writes still go from the `Session` to the PDS, and pushes from isomorphic-git to the knot. OAuth's DPoP tokens are bound to a key in the browser, and a push token is single-use and lasts 60 seconds, so neither can pass through the gateway. - `blobUrl(actor, cid)` gives a `getBlob` URL for an ``, on the gateway when one is set. Unset, the default, every read goes straight to the services as before. Outside a browser (no `document`), every default request and every git request sends `User-Agent: nibling.dev/ (+https://nibling.dev/contact)`, from `user-agent.ts`. The worker and the lexicon publish script send the same string. ## Modules - `identity.ts` resolves a handle or DID to `{ did, handle?, pds }`. It asks the gateway's `resolveMiniDoc` first when a gateway is set, then Slingshot, and falls back to PLC or did:web, with handles resolved through the Bluesky appview. `resolveService` reads a service entry such as `#tangled_knot` from a DID document. `configure` sets every endpoint, and it also takes an `identity` resolver and a `handlerFor` hook so that tests can run with no network. Every default request, identity resolution included, aborts after `timeoutMs` (30 s). A resolved handle, DID, or DID document is reused for 60 s (`IDENTITY_TTL_MS`), a handle and its DID sharing one entry, and `configure` forgets them all. Identical GETs in flight at once share one request, each caller reading its own clone. - `records.ts` reads records from a PDS. `getRecord` and `listRecords` build each record's URI from the repo, collection, and rkey that were asked for, and drop any record whose returned URI names something else, because a PDS can answer with any URI. Write results are checked the same way. `paging.ts` stops every listing at an empty page, a missing or repeated cursor, or 100 pages; `listRecords` also stops at a page shorter than the 100 it asked for, since a PDS sends a cursor even with its last page. - `microcosm.ts` reads Constellation backlinks and Slingshot's record cache, through the gateway when one is set. Both services are optional, so each call returns `undefined` when neither the gateway nor the service answers. - `auth.ts` wraps `@atcute/oauth-browser-client`. It holds the D19 scope string and builds `client-metadata.json`, with the `http://localhost` loopback client for development on `http://127.0.0.1`. `signIn`, `completeSignIn`, `resume`, and `signOut` run only in the browser. - `projects/paths.ts` rejects absolute paths, empty, `.`, and `..` segments, and backslashes, and returns paths in NFC. - `projects/tree.ts` holds pure functions. `diffAgainstBase` computes the overlay that turns a base tree into the current tree, and `applyOverlay` applies one. `blobCid` computes the raw CID that a PDS assigns to a blob. - `projects/store.ts` lists, loads, creates, saves, forks, and deletes `dev.nibling.project` records. A save uploads only the blobs that the loaded record does not already reference, then writes with `swapRecord`. A lost swap throws `ProjectConflictError`. Before any upload, create, save, and fork check the record they are about to write against the lexicon (at most 1,000 entries, the name, description, and tag limits) and each file against 5 MB, and fail with `ERR_NIB_LIMIT`. Records and blobs from a PDS are checked against the lexicon and their CIDs before the store returns them. Loading downloads blobs 8 at a time and stops any blob past 5 MB and the project past `maxBytes` (100 MB), whatever the record claims. A project with a git base needs a `loadBase` hook, which the tangled module supplies. - `feed.ts` calls the index's `getRecent`, `getTrending`, and `getKnown`. Each returns `undefined` when no index is configured or the index is down. - `social/` is D21. `likes.ts` has `like`, `unlike`, `likeCount`, `likers`, and `myLike`, which finds the session's own like by listing its `dev.nibling.feed.like` records, since there is no index to ask "did I like this." `comments.ts` has `comment` and `comments`, which reads Constellation backlinks, fetches each record through Slingshot or its author's PDS, and orders the valid ones into a thread. `follows.ts` has `follow`, `unfollow`, `followers`, `following`, `myFollow`, and `forks`; `following` and `myFollow` list the account's own PDS, and the rest read Constellation backlinks. `import.ts` has `importFollows`, which reads `app.bsky.graph.follow` or `org.tangled.graph.follow` plus legacy `sh.tangled.graph.follow` (merged without duplicates) from the session's own PDS, filters through the index's `getKnown` when one is configured, skips subjects already followed, caps what is left at `max` (500), and writes `dev.nibling.graph.follow` with `via` set to the source collection, in `applyWrites` batches of at most 200. It is idempotent: a second run writes nothing, and a run cut short by `max` continues where it stopped. An index that is configured but does not answer makes it return `{ outcome: 'indexUnavailable' }` without writing. `page.ts` has `actorPage`, which merges `dev.nibling.actor.profile` with `app.bsky.actor.profile`'s `displayName`, `description`, and `avatar` as a fallback, and lists the account's projects and its `org.tangled.repo.declaration` repos; it works for tangled orgs, which are ordinary accounts. `setProfile` writes the session's own profile. Every record this module reads from a PDS or Slingshot is checked against its lexicon before use. ## Testing `pnpm test` runs `node --test` on Node 24's type stripping. `tests/fake-pds.ts` is an in-memory PDS that hosts several accounts and counts calls per method. Set `pds.tamper.uri` or `pds.tamper.cursor` to make it forge record URIs or repeat cursors, as a hostile PDS would. It stores whatever it is given, like a real PDS with an unknown lexicon, so tests can plant records that break limits. Pass `fakeNetwork([pds, ...], services)` to `configure` to route identity and XRPC to fakes. `fakeGateway([pds])` answers the gateway's XRPC routes over the same fakes, and `tests/gateway.test.ts` checks that a configured gateway leaves no direct reads and that a failing one falls back. `node scripts/resolve.ts [--no-slingshot]` resolves an actor against live infrastructure. It only reads. ## Unverified - The PDS's maximum record size. A project without a base can list 1,000 files, and nobody has measured that record against a real PDS. - How soon a PDS garbage-collects blobs that no record references. A save uploads blobs before it writes the record, so a failed or conflicting save leaves orphaned blobs. - Whether `com.atproto.server.getServiceAuth` honors the OAuth scope `rpc:org.tangled.git.receivePack?aud=*` for a knot audience. - The form of a knot's service DID. The tangled module assumes `did:web:`.