mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-27 08:31:00 +00:00
feat(core): implement identifiers module with target creation and normalization
This commit is contained in:
1 parent
358c6c0bc2
commit
7bf304db2c
2 files changed
+320
No files matched your search
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user