Skip to content

Commit

Permalink
Merge pull request #48 from jiftechnify/nip46-new-token-format
Browse files Browse the repository at this point in the history
NIP-46: support new token format
  • Loading branch information
jiftechnify authored Feb 20, 2024
2 parents 9c836e1 + 90110bd commit f0bd3e8
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 20 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Lint
run: yarn lint

- name : Test
run: yarn test

typos:
name: Detect typos
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"fix": "run-s fix:*",
"fix:format": "prettier --write --log-level warn src/**/*.ts",
"fix:js": "eslint --fix src/",
"test": "vitest run",
"build": "node build.js",
"bump-version": "lerna version",
"release": "lerna publish from-package"
Expand Down
2 changes: 1 addition & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const parseSecKey = (secKey: string): { hex: string; bytes: Uint8Array }
return undefined;
};

// parse the given public key of any string format (hex/bech32)
// parse the given public key of any string format (hex/bech32) as hex
// returns undefined if the input is invalid
export const parsePubkey = (pubkey: string): string | undefined => {
if (pubkey.startsWith("npub1")) {
Expand Down
77 changes: 77 additions & 0 deletions src/nip46.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, test } from "vitest";
import { parseConnToken } from "./nip46";

const testPubkey = {
hex: "d1d1747115d16751a97c239f46ec1703292c3b7e9988b9ebdd4ec4705b15ed44",
npub: "npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc",
};

describe("parseConnToken", () => {
describe("should parse bunker:// token", () => {
test("pubkey only", () => {
const { remotePubkey, relayUrls, secretToken } = parseConnToken(`bunker://${testPubkey.npub}`);
expect(remotePubkey).toBe(testPubkey.hex);
expect(relayUrls).toHaveLength(0);
expect(secretToken).toBeUndefined();
});
test("pubkey, relay URLs and secret token", () => {
const { remotePubkey, relayUrls, secretToken } = parseConnToken(
`bunker://${testPubkey.npub}?relay=wss%3A%2F%2Fyabu.me&relay=wss%3A%2F%2Fnrelay.c-stellar.net&secret=123456`,
);
expect(remotePubkey).toBe(testPubkey.hex);
expect(relayUrls).toEqual(["wss://yabu.me", "wss://nrelay.c-stellar.net"]);
expect(secretToken).toBe("123456");
});
});

describe("should parse legacy token", () => {
test("pubkey only", () => {
const { remotePubkey, relayUrls, secretToken } = parseConnToken(`${testPubkey.hex}`);
expect(remotePubkey).toBe(testPubkey.hex);
expect(secretToken).toBeUndefined();
expect(relayUrls).toHaveLength(0);
});
test("pubkey and secret token", () => {
const { remotePubkey, relayUrls, secretToken } = parseConnToken(`${testPubkey.hex}#123456`);
expect(remotePubkey).toBe(testPubkey.hex);
expect(secretToken).toBe("123456");
expect(relayUrls).toHaveLength(0);
});
test("pubkey and relay URLs", () => {
const { remotePubkey, relayUrls, secretToken } = parseConnToken(
`${testPubkey.hex}?relay=wss%3A%2F%2Fyabu.me&relay=wss%3A%2F%2Fnrelay.c-stellar.net`,
);
expect(remotePubkey).toBe(testPubkey.hex);
expect(secretToken).toBeUndefined();
expect(relayUrls).toEqual(["wss://yabu.me", "wss://nrelay.c-stellar.net"]);
});
test("pubkey, relay URLs and secret token", () => {
const { remotePubkey, relayUrls, secretToken } = parseConnToken(
`${testPubkey.hex}#123456?relay=wss%3A%2F%2Fyabu.me&relay=wss%3A%2F%2Fnrelay.c-stellar.net`,
);
expect(remotePubkey).toBe(testPubkey.hex);
expect(secretToken).toBe("123456");
expect(relayUrls).toEqual(["wss://yabu.me", "wss://nrelay.c-stellar.net"]);
});
});

describe("should throw error when invalid connection token", () => {
test("URL token: invalid schema", () => {
expect(() => {
parseConnToken(
`invalid://${testPubkey.npub}?relay=wss%3A%2F%2Fyabu.me&relay=wss%3A%2F%2Fnrelay.c-stellar.net&secret=123456`,
);
}).toThrowError();
});
test("URL token: invalid pubkey", () => {
expect(() => {
parseConnToken(`bunker://hoge`);
}).toThrowError();
});
test("legacy token: invalid pubkey", () => {
expect(() => {
parseConnToken(`hoge#123456`);
}).toThrowError();
});
});
});
71 changes: 52 additions & 19 deletions src/nip46.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,40 +203,72 @@ const nip46RpcResultDecoders: Nip46RpcResultDecoders = {
export type Nip46ConnectionParams = {
remotePubkey: string;
secretToken?: string | undefined;
relayUrls?: string[] | undefined;
relayUrls: string[];
};

const parseConnToken = (token: string): Nip46ConnectionParams => {
export const parseConnToken = (token: string): Nip46ConnectionParams => {
const isUriFormat = token.startsWith("bunker://");
return isUriFormat ? parseUriConnToken(token) : parseLegacyConnToken(token);
};

// parse connection token (new URI format: bunker://<hex-pubkey>?relay=wss://...&relay=wss://...&secret=<optional-secret>)
const parseUriConnToken = (token: string): Nip46ConnectionParams => {
let u: URL;
try {
u = new URL(token);
} catch {
throw Error("invalid connection token");
}
if (u.protocol !== "bunker:") {
throw Error("invalid connection token");
}

const rawPubkey = u.host;
const remotePubkey = parsePubkey(rawPubkey);
if (remotePubkey === undefined) {
throw Error("connection token contains invalid pubkey");
}
const secretToken = u.searchParams.get("secret") ?? undefined;
const relayUrls = u.searchParams.getAll("relay");

return {
remotePubkey,
secretToken,
relayUrls,
};
};

// parse connection token (legacy format: <nsec1...>#<secret>?relay=wss://...&relay=wss://...)
const parseLegacyConnToken = (token: string): Nip46ConnectionParams => {
let parts: {
pubkey: string;
secret?: string;
relays?: string;
};

const splitRelaysPart = (s: string): [string, string] => {
const qi = s.indexOf("?");
const ri = s.indexOf("relay=");
if (ri < qi) {
throw Error("invalid connection token");
}
return s.split("?", 2) as [string, string];
};

if (token.includes("#")) {
const [pubkey, rest] = token.split("#", 2) as [string, string];
if (token.includes("?")) {
const qi = token.indexOf("?");
const ri = token.indexOf("relay=");
if (ri < qi) {
throw Error("invalid connection token");
}
if (rest.includes("?")) {
// <pubkey>#<secret>?<relays>
const [secret, relays] = rest.split("?", 2) as [string, string];
const [secret, relays] = splitRelaysPart(rest);
parts = { pubkey, secret, relays };
} else {
// <pubkey>#<secret>
parts = { pubkey, secret: rest };
}
} else {
if (token.includes("?")) {
const qi = token.indexOf("?");
const ri = token.indexOf("relay=");
if (ri < qi) {
throw Error("invalid connection token");
}
// <pubkey>?<relays>
const [pubkey, relays] = token.split("?", 2) as [string, string];
const [pubkey, relays] = splitRelaysPart(token);
parts = { pubkey, relays };
} else {
// <pubkey>
Expand All @@ -249,10 +281,11 @@ const parseConnToken = (token: string): Nip46ConnectionParams => {
throw Error("connection token contains invalid pubkey");
}

const relayUrls = parts.relays
?.replace("relay=", "")
.split("&relay=")
.map((r) => decodeURIComponent(r));
const relayUrls =
parts.relays
?.replace("relay=", "")
.split("&relay=")
.map((r) => decodeURIComponent(r)) ?? [];
try {
relayUrls?.forEach((r) => new URL(r));
} catch {
Expand Down

0 comments on commit f0bd3e8

Please sign in to comment.