# publish-wrap-with-auth.ts

> **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [all examples](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/examples/README.md) · source: `nostr-dev/sdk/examples/publish-wrap-with-auth.ts` · snapshot 2026-10-10

Publish a pre-signed event to a relay that demands NIP-42 AUTH, without re-signing it.

```ts
// Publish a fully-signed event (e.g. a NIP-59 gift wrap) to a relay that
// requires NIP-42 AUTH, WITHOUT letting any helper re-sign the event itself.
// `nak event --sec ... --auth` rebuilds the event when the --sec pubkey
// doesn't match event.pubkey, which corrupts gift wraps. This script keeps
// the wrap byte-for-byte and only uses the key to satisfy AUTH.
//
// Usage:
//   NOSTR_SECRET_KEY=nsec1... npx tsx publish-wrap-with-auth.ts <event.json> <relay-url> [more relays]

import WebSocket from "ws";
import * as fs from "fs";
import { finalizeEvent } from "nostr-tools/pure";
import { nip19 } from "nostr-tools";

const eventPath = process.argv[2];
const relayUrls = process.argv.slice(3);
if (!eventPath || relayUrls.length === 0) {
  console.error("usage: publish-wrap-with-auth.ts <event.json> <relay-url> [more relays]");
  process.exit(2);
}

const nsec = process.env.NOSTR_SECRET_KEY;
if (!nsec) {
  console.error("NOSTR_SECRET_KEY env var must be set (nsec1... or hex)");
  process.exit(2);
}

const sk: Uint8Array = (() => {
  if (nsec.startsWith("nsec1")) {
    const { type, data } = nip19.decode(nsec);
    if (type !== "nsec") throw new Error("not an nsec");
    return data as Uint8Array;
  }
  // hex
  const bytes = new Uint8Array(32);
  for (let i = 0; i < 32; i++) bytes[i] = parseInt(nsec.slice(i * 2, i * 2 + 2), 16);
  return bytes;
})();

const evt = JSON.parse(fs.readFileSync(eventPath, "utf-8"));

async function publishToRelay(url: string): Promise<{ url: string; result: string }> {
  return new Promise((resolve) => {
    const ws = new WebSocket(url);
    let publishedOnce = false;

    const finish = (result: string) => {
      try { ws.close(); } catch {}
      resolve({ url, result });
    };

    const timeout = setTimeout(() => finish("TIMEOUT (15s)"), 15000);

    let pendingChallenge: string | null = null;
    let eventSent = false;
    let authSent = false;
    const trySendEvent = () => {
      if (!eventSent) {
        eventSent = true;
        ws.send(JSON.stringify(["EVENT", evt]));
      }
    };
    const respondToChallenge = (challenge: string) => {
      if (authSent) return;
      authSent = true;
      const authEvent = finalizeEvent(
        {
          kind: 22242,
          created_at: Math.floor(Date.now() / 1000),
          tags: [
            ["relay", url],
            ["challenge", challenge],
          ],
          content: "",
        },
        sk
      );
      ws.send(JSON.stringify(["AUTH", authEvent]));
    };

    ws.on("open", () => {
      // Wait ~500 ms for a proactive AUTH challenge before sending EVENT,
      // so AUTH-first relays don't reject us with auth-required.
      setTimeout(() => {
        if (pendingChallenge) respondToChallenge(pendingChallenge);
        trySendEvent();
      }, 500);
    });

    ws.on("message", (data) => {
      let msg: any;
      try { msg = JSON.parse(data.toString()); } catch { return; }
      if (!Array.isArray(msg)) return;
      console.error(`[${url}] <- ${JSON.stringify(msg).slice(0, 200)}`);

      if (msg[0] === "AUTH") {
        pendingChallenge = msg[1];
        respondToChallenge(msg[1]);
        // After AUTH, retry EVENT if we already sent once and got rejected.
        if (eventSent) {
          eventSent = false;
          setTimeout(trySendEvent, 100);
        }
      } else if (msg[0] === "OK" && msg[1] === evt.id) {
        const ok = msg[2];
        const note = msg[3] ?? "";
        if (!ok && /auth-required/i.test(note)) {
          // Wait for the relay to send us an AUTH challenge if it hasn't already.
          return;
        }
        clearTimeout(timeout);
        finish(ok ? `success` : `rejected: ${note}`);
      } else if (msg[0] === "NOTICE") {
        console.error(`[${url}] NOTICE: ${msg[1]}`);
      } else if (msg[0] === "CLOSED") {
        clearTimeout(timeout);
        finish(`CLOSED: ${msg[2] ?? ""}`);
      }
    });

    ws.on("error", (err) => {
      clearTimeout(timeout);
      finish(`error: ${err.message}`);
    });
  });
}

(async () => {
  console.error(`event id: ${evt.id}`);
  console.error(`event kind: ${evt.kind}`);
  console.error(`event pubkey: ${evt.pubkey} (preserved, NOT re-signed)`);
  console.error("");
  for (const url of relayUrls) {
    const { result } = await publishToRelay(url);
    console.log(`${url}\t${result}`);
  }
})();
```
