feat(core): implement identifiers module with target creation and normalization

This commit is contained in:
thePR0M3TH3AN committed 2026-07-26 19:02:33 -04:00
1 parent 358c6c0bc2
commit 7bf304db2c
2 files changed
+320

No files matched your search

+189
View File
@@ -0,0 +1,189 @@
// Core identifiers for Nostr Governance targets
//
// These are the fundamental building blocks for identifying targets in the
// governance system. Each target type has a specific structure that allows
// for consistent identification and comparison.
/**
* @typedef {Object} UserTarget
* @property {"user"} type
* @property {string} pubkey - 64-character hex pubkey
*/
/**
* @typedef {Object} EventTarget
* @property {"event"} type
* @property {string} id - 64-character hex event ID
*/
/**
* @typedef {Object} AddressTarget
* @property {"address"} type
* @property {string} kind - Event kind as string
* @property {string} pubkey - 64-character hex pubkey of author
* @property {string} identifier - d-tag identifier for parameterized replaceable events
*/
/**
* @typedef {UserTarget|EventTarget|AddressTarget} GovernanceTarget
*/
/**
* Normalize a Nostr pubkey to lowercase hex format
* @param {string} pubkey - Pubkey in either hex or npub format
* @returns {string} 64-character lowercase hex pubkey, or empty string if invalid
*/
export function normalizePubkey(pubkey) {
if (typeof pubkey !== "string") return "";
const trimmed = pubkey.trim();
if (!trimmed) return "";
// Already a hex pubkey
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return trimmed.toLowerCase();
}
// Try to decode npub
try {
// In a real implementation, we would use a Nostr library here
// For now, we'll just return a placeholder
// const decoded = NostrTools.nip19.decode(trimmed);
// if (decoded?.type === "npub") {
// return decoded.data.toLowerCase();
// }
return ""; // Placeholder for now
} catch (error) {
return "";
}
}
/**
* Normalize a Nostr event ID to lowercase hex format
* @param {string} id - Event ID in either hex or note/nevent format
* @returns {string} 64-character lowercase hex event ID, or empty string if invalid
*/
export function normalizeEventId(id) {
if (typeof id !== "string") return "";
const trimmed = id.trim();
if (!trimmed) return "";
// Already a hex event ID
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return trimmed.toLowerCase();
}
// Try to decode note/nevent
try {
// In a real implementation, we would use a Nostr library here
// For now, we'll just return a placeholder
// const decoded = NostrTools.nip19.decode(trimmed);
// if (decoded?.type === "note" || decoded?.type === "nevent") {
// return (decoded.data?.id || decoded.data).toLowerCase();
// }
return ""; // Placeholder for now
} catch (error) {
return "";
}
}
/**
* Validate and normalize an address target
* @param {string|number} kind - Event kind
* @param {string} pubkey - Author pubkey in hex or npub format
* @param {string} identifier - d-tag identifier
* @returns {AddressTarget|null} Normalized address target or null if invalid
*/
export function normalizeAddress(kind, pubkey, identifier) {
// Validate kind
const kindStr = String(kind).trim();
if (!kindStr) return null;
// Validate pubkey
const normalizedPubkey = normalizePubkey(pubkey);
if (!normalizedPubkey) return null;
// Validate identifier
const identifierStr = String(identifier).trim();
if (!identifierStr) return null;
return {
type: "address",
kind: kindStr,
pubkey: normalizedPubkey,
identifier: identifierStr
};
}
/**
* Create a user target
* @param {string} pubkey - User pubkey in hex or npub format
* @returns {UserTarget|null} User target or null if invalid
*/
export function createUserTarget(pubkey) {
const normalizedPubkey = normalizePubkey(pubkey);
if (!normalizedPubkey) return null;
return {
type: "user",
pubkey: normalizedPubkey
};
}
/**
* Create an event target
* @param {string} id - Event ID in hex or note/nevent format
* @returns {EventTarget|null} Event target or null if invalid
*/
export function createEventTarget(id) {
const normalizedId = normalizeEventId(id);
if (!normalizedId) return null;
return {
type: "event",
id: normalizedId
};
}
/**
* Get a string key for a target for use in maps/sets
* @param {GovernanceTarget} target
* @returns {string} Unique string key for the target
*/
export function getTargetKey(target) {
// Type guard functions to help TypeScript
/**
* @param {any} t
* @returns {t is UserTarget}
*/
function isUserTarget(t) {
return t && typeof t === "object" && t.type === "user";
}
/**
* @param {any} t
* @returns {t is EventTarget}
*/
function isEventTarget(t) {
return t && typeof t === "object" && t.type === "event";
}
/**
* @param {any} t
* @returns {t is AddressTarget}
*/
function isAddressTarget(t) {
return t && typeof t === "object" && t.type === "address";
}
if (isUserTarget(target)) {
return `user:${target.pubkey}`;
} else if (isEventTarget(target)) {
return `event:${target.id}`;
} else if (isAddressTarget(target)) {
return `address:${target.kind}:${target.pubkey}:${target.identifier}`;
} else {
throw new Error(`Unknown target type: ${JSON.stringify(target)}`);
}
}
+131
View File
@@ -0,0 +1,131 @@
import { describe, it, expect } from "vitest";
import {
normalizePubkey,
normalizeEventId,
normalizeAddress,
createUserTarget,
createEventTarget,
getTargetKey
} from "../src/identifiers.js";
describe("identifiers", () => {
describe("normalizePubkey", () => {
it("should normalize hex pubkeys to lowercase", () => {
const hexPubkey = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
const expected = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
expect(normalizePubkey(hexPubkey)).toBe(expected);
});
it("should return empty string for invalid pubkeys", () => {
expect(normalizePubkey("")).toBe("");
expect(normalizePubkey(/** @type {any} */ (null))).toBe("");
expect(normalizePubkey(/** @type {any} */ (undefined))).toBe("");
expect(normalizePubkey("invalid")).toBe("");
expect(normalizePubkey("abc123")).toBe(""); // Too short
});
});
describe("normalizeEventId", () => {
it("should normalize hex event IDs to lowercase", () => {
const hexId = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
const expected = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
expect(normalizeEventId(hexId)).toBe(expected);
});
it("should return empty string for invalid event IDs", () => {
expect(normalizeEventId("")).toBe("");
expect(normalizeEventId(/** @type {any} */ (null))).toBe("");
expect(normalizeEventId(/** @type {any} */ (undefined))).toBe("");
expect(normalizeEventId("invalid")).toBe("");
expect(normalizeEventId("abc123")).toBe(""); // Too short
});
});
describe("normalizeAddress", () => {
it("should create a valid address target", () => {
const result = normalizeAddress(
30023,
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"test-identifier"
);
expect(result).toEqual({
type: "address",
kind: "30023",
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
identifier: "test-identifier"
});
});
it("should return null for invalid addresses", () => {
expect(normalizeAddress("", "valid-pubkey", "identifier")).toBeNull();
expect(normalizeAddress(30023, "", "identifier")).toBeNull();
expect(normalizeAddress(30023, "invalid-pubkey", "identifier")).toBeNull();
expect(normalizeAddress(30023, "valid-pubkey", "")).toBeNull();
});
});
describe("createUserTarget", () => {
it("should create a valid user target", () => {
const result = createUserTarget(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
);
expect(result).toEqual({
type: "user",
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
});
});
it("should return null for invalid pubkeys", () => {
expect(createUserTarget("")).toBeNull();
expect(createUserTarget("invalid")).toBeNull();
});
});
describe("createEventTarget", () => {
it("should create a valid event target", () => {
const result = createEventTarget(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
);
expect(result).toEqual({
type: "event",
id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
});
});
it("should return null for invalid event IDs", () => {
expect(createEventTarget("")).toBeNull();
expect(createEventTarget("invalid")).toBeNull();
});
});
describe("getTargetKey", () => {
it("should generate correct keys for user targets", () => {
const target = /** @type {import('../src/identifiers.js').UserTarget} */ ({
type: "user",
pubkey: "test-pubkey"
});
expect(getTargetKey(target)).toBe("user:test-pubkey");
});
it("should generate correct keys for event targets", () => {
const target = /** @type {import('../src/identifiers.js').EventTarget} */ ({
type: "event",
id: "test-id"
});
expect(getTargetKey(target)).toBe("event:test-id");
});
it("should generate correct keys for address targets", () => {
const target = /** @type {import('../src/identifiers.js').AddressTarget} */ ({
type: "address",
kind: "30023",
pubkey: "test-pubkey",
identifier: "test-identifier"
});
expect(getTargetKey(target)).toBe("address:30023:test-pubkey:test-identifier");
});
});
});