Files
bitvid/js/services/authProviders/nip46.js
T

267 lines
8.4 KiB
JavaScript

import { devLogger } from "../../utils/logger.js";
function summarizeHexForLog(value) {
if (typeof value !== "string" || !value.trim()) {
return "";
}
const normalized = value.trim().toLowerCase();
if (normalized.length <= 12) {
return `${normalized} (len:${normalized.length})`;
}
return `${normalized.slice(0, 8)}…${normalized.slice(-4)} (len:${normalized.length})`;
}
function summarizeSecretForLog(value) {
if (typeof value !== "string" || !value.trim()) {
return "<empty>";
}
const trimmed = value.trim();
const visible = trimmed.length <= 4 ? "*".repeat(trimmed.length) : `${"*".repeat(3)}…`;
return `${visible} (len:${trimmed.length})`;
}
function sanitizeNip46UriForLog(value) {
if (typeof value !== "string" || !value) {
return "";
}
return value.replace(/(secret=)([^&#]+)/gi, "$1***");
}
const PROVIDER_ID = "nip46";
const PROVIDER_LABEL = "remote signer (nip-46)";
const PROVIDER_DESCRIPTION =
"Connect to a remote signer via Nostr Connect for hardware or mobile approvals.";
const PROVIDER_CAPABILITIES = Object.freeze([
Object.freeze({
id: "signing",
label: "Signs events remotely",
variant: "info",
}),
Object.freeze({
id: "multi-device",
label: "Works across browsers",
variant: "neutral",
}),
]);
const PROVIDER_BUTTON = Object.freeze({
variant: "primary",
});
const PROVIDER_MESSAGES = Object.freeze({
loading: "Connecting to remote signer…",
slow: "Waiting for the signer to approve…",
error: "Failed to connect to the remote signer. Please try again.",
});
function normalizePubkey(result) {
if (typeof result === "string") {
return result;
}
if (!result || typeof result !== "object") {
return "";
}
if (typeof result.pubkey === "string") {
return result.pubkey;
}
if (typeof result.publicKey === "string") {
return result.publicKey;
}
return "";
}
function normalizeOptions(options) {
if (!options || typeof options !== "object") {
return {
connectionString: "",
remember: true,
reuseStored: false,
mode: "handshake",
onHandshakePrepared: null,
onStatus: null,
onAuthUrl: null,
metadata: {},
secret: "",
permissions: "",
relays: [],
handshakeTimeoutMs: undefined,
};
}
return {
connectionString:
typeof options.connectionString === "string" ? options.connectionString.trim() : "",
remember: options.remember !== false,
reuseStored: options.reuseStored === true,
mode: options.mode === "manual" ? "manual" : "handshake",
onHandshakePrepared:
typeof options.onHandshakePrepared === "function" ? options.onHandshakePrepared : null,
onStatus: typeof options.onStatus === "function" ? options.onStatus : null,
onAuthUrl: typeof options.onAuthUrl === "function" ? options.onAuthUrl : null,
metadata: options.metadata && typeof options.metadata === "object" ? options.metadata : {},
secret:
typeof options.secret === "string" && options.secret.trim() ? options.secret.trim() : "",
permissions:
typeof options.permissions === "string" && options.permissions.trim()
? options.permissions.trim()
: "",
relays: Array.isArray(options.relays) ? options.relays : [],
handshakeTimeoutMs: Number.isFinite(options.handshakeTimeoutMs)
? Number(options.handshakeTimeoutMs)
: undefined,
};
}
export default {
id: PROVIDER_ID,
label: PROVIDER_LABEL,
description: PROVIDER_DESCRIPTION,
eyebrow: "Nostr Connect",
tone: "accent",
badgeVariant: "info",
capabilities: PROVIDER_CAPABILITIES,
button: PROVIDER_BUTTON,
messages: PROVIDER_MESSAGES,
errorMessage: PROVIDER_MESSAGES.error,
async login({ nostrClient, options } = {}) {
if (!nostrClient || typeof nostrClient !== "object") {
const error = new Error("Remote signer login is not available.");
error.code = "provider-unavailable";
throw error;
}
if (typeof nostrClient.connectRemoteSigner !== "function") {
const error = new Error("Remote signer support is unavailable in this environment.");
error.code = "provider-unavailable";
throw error;
}
const normalized = normalizeOptions(options);
devLogger.debug("[nip46] Login requested", {
mode: normalized.mode,
reuseStored: normalized.reuseStored,
remember: normalized.remember,
hasConnectionString: Boolean(normalized.connectionString),
relayCount: normalized.relays.length,
permissions: normalized.permissions || null,
secret: summarizeSecretForLog(normalized.secret),
});
let result = null;
if (normalized.reuseStored) {
if (typeof nostrClient.useStoredRemoteSigner !== "function") {
const error = new Error("No stored remote signer is available on this device.");
error.code = "no-stored-session";
throw error;
}
devLogger.debug("[nip46] Reusing stored remote signer session");
result = await nostrClient.useStoredRemoteSigner();
devLogger.debug("[nip46] Stored remote signer returned", {
pubkey: summarizeHexForLog(normalizePubkey(result)),
});
} else if (normalized.mode === "manual") {
const uri = normalized.connectionString;
if (!uri) {
const error = new Error("A connection string is required to continue.");
error.code = "connection-required";
throw error;
}
devLogger.debug("[nip46] Initiating manual remote signer connect", {
uri: sanitizeNip46UriForLog(uri),
remember: normalized.remember,
});
result = await nostrClient.connectRemoteSigner({
connectionString: uri,
remember: normalized.remember,
onAuthUrl: normalized.onAuthUrl,
onStatus: normalized.onStatus,
});
devLogger.debug("[nip46] Manual connect completed", {
pubkey: summarizeHexForLog(normalizePubkey(result)),
});
} else {
if (normalized.onStatus) {
try {
normalized.onStatus({
phase: "handshake",
state: "preparing",
message: "Generating remote signer connect link…",
});
} catch (callbackError) {
devLogger.warn("[nip46] Handshake status callback threw:", callbackError);
}
}
const handshake = await nostrClient.prepareRemoteSignerHandshake({
metadata: normalized.metadata,
relays: normalized.relays,
secret: normalized.secret,
permissions: normalized.permissions,
});
devLogger.debug("[nip46] Prepared remote signer handshake", {
relays: Array.isArray(handshake.relays) ? handshake.relays : [],
permissions: handshake.permissions || null,
clientPublicKey: summarizeHexForLog(handshake.clientPublicKey),
secret: summarizeSecretForLog(handshake.secret),
metadataKeys: Object.keys(handshake.metadata || {}),
connectionString: sanitizeNip46UriForLog(handshake.connectionString),
});
if (normalized.onHandshakePrepared) {
try {
normalized.onHandshakePrepared(handshake);
} catch (callbackError) {
devLogger.warn("[nip46] Handshake prepared callback threw:", callbackError);
}
}
devLogger.debug("[nip46] Connecting to remote signer with prepared handshake", {
remember: normalized.remember,
handshakeAlgorithm: handshake.algorithm || null,
connectionString: sanitizeNip46UriForLog(handshake.connectionString),
});
result = await nostrClient.connectRemoteSigner({
connectionString: handshake.connectionString,
remember: normalized.remember,
clientPrivateKey: handshake.clientPrivateKey,
clientPublicKey: handshake.clientPublicKey,
relays: handshake.relays,
secret: handshake.secret,
permissions: handshake.permissions,
metadata: handshake.metadata,
onAuthUrl: normalized.onAuthUrl,
onStatus: normalized.onStatus,
handshakeTimeoutMs: normalized.handshakeTimeoutMs,
});
if (result && typeof result === "object") {
result.handshake = handshake;
}
}
const pubkey = normalizePubkey(result);
devLogger.debug("[nip46] Login finished", {
pubkey: summarizeHexForLog(pubkey),
hasSigner: Boolean(result && typeof result === "object" && result.signer),
});
return {
authType: PROVIDER_ID,
pubkey,
signer: result && typeof result === "object" ? result.signer || null : null,
};
},
};