mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
Signed-off-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@buzz.block.builderlab.xyz> Signed-off-by: bb-expert <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz>
215 lines
7.0 KiB
TypeScript
215 lines
7.0 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
import { safeNpub } from "@/shared/lib/nostrUtils";
|
|
|
|
export const HOSTED_COMMUNITY_SUFFIX = "communities.buzz.xyz";
|
|
export const HOSTED_COMMUNITY_LIMIT = 5;
|
|
export const VALID_HOSTED_COMMUNITY_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
|
export type BuilderlabAuth = {
|
|
email?: string;
|
|
expiresAt: string;
|
|
};
|
|
|
|
export type HostedCommunityApiError = {
|
|
code?: string;
|
|
message?: string;
|
|
setup_needed?: boolean;
|
|
};
|
|
|
|
export type HostedNostrIdentity = {
|
|
npub?: string;
|
|
pubkey_hex?: string;
|
|
};
|
|
|
|
export type HostedIdentityResponse = {
|
|
identity?: HostedNostrIdentity;
|
|
error?: HostedCommunityApiError;
|
|
correlation_id?: string;
|
|
};
|
|
|
|
export type HostedCommunity = {
|
|
id?: string;
|
|
name?: string;
|
|
slug?: string;
|
|
normalized_host?: string;
|
|
owner_pubkey?: string;
|
|
archived_at?: string | null;
|
|
};
|
|
|
|
export type HostedCommunitiesResponse = {
|
|
communities?: HostedCommunity[];
|
|
error?: HostedCommunityApiError;
|
|
correlation_id?: string;
|
|
};
|
|
|
|
export type HostedCommunityAvailabilityResponse = {
|
|
available?: boolean;
|
|
normalized_host?: string;
|
|
error?: HostedCommunityApiError;
|
|
correlation_id?: string;
|
|
};
|
|
|
|
export type HostedCommunityMutationResponse = {
|
|
community?: HostedCommunity;
|
|
error?: HostedCommunityApiError;
|
|
correlation_id?: string;
|
|
};
|
|
|
|
export type HostedCommunityAccount = {
|
|
communities: HostedCommunity[];
|
|
identity: HostedNostrIdentity | null;
|
|
};
|
|
|
|
export function hostedCommunityErrorMessage(
|
|
error: HostedCommunityApiError | undefined,
|
|
correlationId: string | undefined,
|
|
fallback: string,
|
|
) {
|
|
const messages: Record<string, string> = {
|
|
missing_mapping: "Connect your Buzz identity before creating a community.",
|
|
invalid_name: "Use lowercase letters, numbers, and hyphens.",
|
|
taken: "That Buzz address is already taken.",
|
|
limit_reached: `You've reached the limit of ${HOSTED_COMMUNITY_LIMIT} hosted communities.`,
|
|
relay_unavailable: "Community provisioning is temporarily unavailable.",
|
|
identity_already_bound:
|
|
"This Builderlab account is connected to another Buzz identity.",
|
|
pubkey_already_bound:
|
|
"This Buzz identity is connected to another Builderlab account.",
|
|
not_owner: "Only the community owner can do that.",
|
|
transferee_not_registered:
|
|
"That person needs a connected Buzz identity before you can transfer ownership to them.",
|
|
};
|
|
const message = messages[error?.code ?? ""] ?? error?.message ?? fallback;
|
|
return correlationId
|
|
? `${message} Correlation ID: ${correlationId}`
|
|
: message;
|
|
}
|
|
|
|
export function hostedCommunityRelayUrl(community: HostedCommunity) {
|
|
const host = community.normalized_host?.trim();
|
|
return host ? `wss://${host.replace(/^wss?:\/\//, "")}` : null;
|
|
}
|
|
|
|
const BOUND_KEY_HEX_REGEX = /^[0-9a-f]{64}$/;
|
|
|
|
/**
|
|
* The ONE operational form of a hosted identity's bound key: the trimmed,
|
|
* lowercased `pubkey_hex` when that field carries a 64-character hex key,
|
|
* null otherwise.
|
|
*
|
|
* `pubkey_hex` is the authoritative field the mismatch gate and every
|
|
* hosted-community operation act on, so its accepted shape is defined here
|
|
* once and is hex-only. An `npub` spelling belongs to the server's separate
|
|
* display field: one stored in the hex field is not a hex key and fails
|
|
* closed here instead of widening the authorization domain beyond what
|
|
* comparisons see. Whitespace-padded or mixed-case hex normalizes to the
|
|
* identical value on both sides of every comparison, so a key that is the
|
|
* same after normalization never reads as a mismatch demanding a
|
|
* delete/rebind of an identity the device already holds. Consumers
|
|
* normalize the local key through this same function before comparing.
|
|
*/
|
|
export function normalizedBoundKeyHex(
|
|
value: string | null | undefined,
|
|
): string | null {
|
|
if (!value) return null;
|
|
const normalized = value.trim().toLowerCase();
|
|
return BOUND_KEY_HEX_REGEX.test(normalized) ? normalized : null;
|
|
}
|
|
|
|
/**
|
|
* The canonical npub of the hosted identity's authoritative bound key, or
|
|
* null when the payload carries no key the app can act on.
|
|
*
|
|
* `pubkey_hex` is the field the mismatch gate and every hosted-community
|
|
* operation act on, and it is only usable when it normalizes to a hex key
|
|
* (`normalizedBoundKeyHex`) — the server-sent `npub` is an independent,
|
|
* unverified spelling and is never accepted in its place. The returned npub
|
|
* is derived from that same normalized key, so the displayed account
|
|
* identity can never disagree with the binding the decisions act on. Hosted
|
|
* surfaces derive their connected/readiness/create/connect gates from the
|
|
* normalized key instead of identity-object presence, so an identity payload
|
|
* without a usable authoritative key (missing, empty, invalid/non-hex,
|
|
* wrong-length, npub-only, or an npub stored in the hex field) fails closed
|
|
* into recovery rather than presenting the account as connected.
|
|
*/
|
|
export function usableBoundIdentityNpub(
|
|
identity: HostedNostrIdentity | null | undefined,
|
|
): string | null {
|
|
const hex = normalizedBoundKeyHex(identity?.pubkey_hex);
|
|
return hex === null ? null : safeNpub(hex);
|
|
}
|
|
|
|
export function getBuilderlabAuth() {
|
|
return invoke<BuilderlabAuth | null>("get_builderlab_auth");
|
|
}
|
|
|
|
export function cancelBuilderlabLogin() {
|
|
return invoke<void>("cancel_builderlab_login");
|
|
}
|
|
|
|
export function clearBuilderlabAuth() {
|
|
return invoke<void>("clear_builderlab_auth");
|
|
}
|
|
|
|
export function startBuilderlabLogin() {
|
|
return invoke<BuilderlabAuth>("start_builderlab_login");
|
|
}
|
|
|
|
export async function loadHostedCommunityAccount(): Promise<HostedCommunityAccount> {
|
|
const [identityResponse, communitiesResponse] = await Promise.all([
|
|
invoke<HostedIdentityResponse>("get_builderlab_nostr_identity"),
|
|
invoke<HostedCommunitiesResponse>("list_builderlab_communities"),
|
|
]);
|
|
if (
|
|
identityResponse.error &&
|
|
identityResponse.error.code !== "unauthorized" &&
|
|
!identityResponse.error.setup_needed
|
|
) {
|
|
throw new Error(
|
|
hostedCommunityErrorMessage(
|
|
identityResponse.error,
|
|
identityResponse.correlation_id,
|
|
"Could not load the connected Buzz identity.",
|
|
),
|
|
);
|
|
}
|
|
if (communitiesResponse.error && !communitiesResponse.error.setup_needed) {
|
|
throw new Error(
|
|
hostedCommunityErrorMessage(
|
|
communitiesResponse.error,
|
|
communitiesResponse.correlation_id,
|
|
"Could not load communities.",
|
|
),
|
|
);
|
|
}
|
|
return {
|
|
identity: identityResponse.identity ?? null,
|
|
communities: communitiesResponse.communities ?? [],
|
|
};
|
|
}
|
|
|
|
export function bindBuilderlabIdentity() {
|
|
return invoke<HostedIdentityResponse>("bind_builderlab_nostr_identity");
|
|
}
|
|
|
|
export function deleteBuilderlabIdentity() {
|
|
return invoke<HostedIdentityResponse>("delete_builderlab_nostr_identity");
|
|
}
|
|
|
|
export function checkHostedCommunityName(name: string) {
|
|
return invoke<HostedCommunityAvailabilityResponse>(
|
|
"check_builderlab_community_name",
|
|
{ name },
|
|
);
|
|
}
|
|
|
|
export function createHostedCommunity(name: string) {
|
|
return invoke<HostedCommunityMutationResponse>(
|
|
"create_builderlab_community",
|
|
{
|
|
name,
|
|
},
|
|
);
|
|
}
|