Skip to content

Document the NIP-98 agent-write pattern (Nostr key → WebID → pod write), + a few papercuts #79

Description

@melvincarvalho

What

jspod already supports giving an autonomous agent scoped, key-authenticated read/write to a pod — no OAuth, no JWT, just a Nostr key. We dogfooded the full loop and it works cleanly, but the recipe isn't documented and we hit a few avoidable papercuts. Filing so it can become docs (and maybe a couple of small DX fixes).

The pattern

An agent holds its own Nostr key. It reads a pod resource publicly and writes with a NIP-98 (kind 27235) Schnorr signature. The server resolves the signing pubkey to an identity two ways:

  1. As a WebID — if the pubkey is published in the pod owner's profile as a CID v1 verificationMethod referenced from authentication. jspod's profile already ships the cid / lws context with the verificationMethod / publicKeyMultibase / publicKeyJwk slots — they're just empty by default.
  2. As did:nostr:<pubkey> — the fallback when the key isn't in that pod's owner profile (the cross-pod / "guest" case). Grant this identity in a WAC ACL.

Recipe

  1. Add the agent's Nostr key to a WebID profile as a Multikey verificationMethod, listed in authentication. Encoding for publicKeyMultibase: f + e701 + 02 + the x-only pubkey hex.
  2. To let the agent write a resource it doesn't own, add a resource .acl granting acl:agent = did:nostr:<pubkey> the acl:Write mode.
  3. Sign each write: a NIP-98 event with u + method tags, sent as Authorization: Nostr <base64(event)>.
  4. General edit: the agent does GET resource, modifies the JSON-LD, then PUTs it back signed. Two primitives (get / put) plus the agent's own reasoning give full CRUD — no per-verb endpoints needed.

Papercuts worth documenting / fixing

  1. The .acl must live in the live --root. Writing it into a same-named pod-data/ directory elsewhere silently no-ops, because the server reads the runtime data root. Easy to get wrong when multiple checkouts exist.
  2. NIP-98 payload tag vs body re-serialization. The verifier hashes the parsed-and-re-serialized request body, so a payload SHA-256 computed over the client's exact bytes mismatches. Omitting the optional payload tag works (the signature still binds URL + method + freshness), but this should be documented — or the verifier should hash the raw body.
  3. --no-bootstrap pods render blank in a browser. The data-browser assets (data-browser-folder.js / .css) aren't placed in /public, so an HTML GET 404s them and shows nothing. Either document "it's headless, copy these in," or serve them from the package regardless of bootstrap.
  4. A resource .acl overrides inherited container defaults. You must restate the owner's Control and public Read alongside the new grant, or they're lost for that one resource.
  5. acl:agent did:nostr matching is exact-string — worth a note so people get the casing/format right.

Suggestion

A small CLI helper would turn this from "hand-edit JSON-LD" into one command, e.g. jspod profile add-nostr-key <pubkey> and/or jspod grant <webid-or-did> <mode> <resource>.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions