Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@moonlight-protocol/provider-console",
"version": "0.3.0",
"version": "0.3.1",
"license": "MIT",
"tasks": {
"dev": "deno run --allow-all --allow-env --watch src/server.ts",
Expand Down
4 changes: 4 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { metadataView } from "./views/setup/metadata.ts";
import { fundView } from "./views/setup/fund.ts";
import { joinView } from "./views/setup/join.ts";

// Public KYC/KYB submission — no auth, isolated from operator session
import { entitiesRegisterView } from "./views/entities/register.ts";

// Initialize analytics (NOOP in dev)
initAnalytics();
initTracer({ endpoint: OTEL_ENDPOINT, auth: OTEL_AUTH });
Expand All @@ -27,6 +30,7 @@ route("/setup/metadata", metadataView);
route("/setup/fund", fundView);
route("/setup/join", joinView);
route("/recover", recoverView);
route("/entities/register", entitiesRegisterView);

// Root — redirect based on auth state
route("/", () => {
Expand Down
61 changes: 36 additions & 25 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,10 @@ export async function listPps(): Promise<PpInfo[]> {
}

export async function deletePp(publicKey: string): Promise<void> {
const res = await platformFetch("/dashboard/pp/delete", {
method: "POST",
body: JSON.stringify({ publicKey }),
});
const res = await platformFetch(
`/providers/${encodeURIComponent(publicKey)}`,
{ method: "DELETE" },
);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.message || "Failed to delete provider");
Expand Down Expand Up @@ -234,10 +234,14 @@ export async function joinCouncil(data: {
timestamp: number;
};
}): Promise<{ joinRequestId: string; status: string }> {
const res = await platformFetch("/dashboard/council/join", {
method: "POST",
body: JSON.stringify(data),
});
const { ppPublicKey, ...rest } = data;
const res = await platformFetch(
`/providers/${encodeURIComponent(ppPublicKey)}/council/join`,
{
method: "POST",
body: JSON.stringify(rest),
},
);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.message || "Failed to join council");
Expand All @@ -263,9 +267,7 @@ export async function getCouncilMembership(
ppPublicKey: string,
): Promise<CouncilMembership | null> {
const res = await platformFetch(
`/dashboard/council/membership?ppPublicKey=${
encodeURIComponent(ppPublicKey)
}`,
`/providers/${encodeURIComponent(ppPublicKey)}/council/membership`,
);
if (!res.ok) throw new Error("Failed to retrieve membership");
const { data } = await res.json();
Expand All @@ -280,10 +282,13 @@ export async function getCouncilMembership(
export async function checkMembershipStatus(
ppPublicKey: string,
): Promise<"ACTIVE" | "PENDING" | "REJECTED"> {
const res = await platformFetch("/dashboard/council/membership", {
method: "POST",
body: JSON.stringify({ ppPublicKey }),
});
const res = await platformFetch(
`/providers/${encodeURIComponent(ppPublicKey)}/council/membership`,
{
method: "POST",
body: JSON.stringify({}),
},
);
if (!res.ok) return "PENDING";
const { data } = await res.json();
return data?.status ?? "PENDING";
Expand All @@ -302,7 +307,7 @@ export async function getTreasury(
ppPublicKey: string,
): Promise<TreasuryData> {
const res = await platformFetch(
`/dashboard/treasury?ppPublicKey=${encodeURIComponent(ppPublicKey)}`,
`/providers/${encodeURIComponent(ppPublicKey)}/treasury`,
);
if (!res.ok) throw new Error("Failed to fetch treasury info");
const { data } = await res.json();
Expand Down Expand Up @@ -338,11 +343,10 @@ export async function getMetrics(
ppPublicKey: string,
rangeMin: number,
): Promise<MetricsResponse> {
const qs = new URLSearchParams({
ppPublicKey,
rangeMin: String(rangeMin),
});
const res = await platformFetch(`/dashboard/metrics?${qs}`);
const qs = new URLSearchParams({ rangeMin: String(rangeMin) });
const res = await platformFetch(
`/providers/${encodeURIComponent(ppPublicKey)}/metrics?${qs}`,
);
if (!res.ok) throw new Error("Failed to fetch metrics");
const body = await res.json();
return body.data as MetricsResponse;
Expand Down Expand Up @@ -371,9 +375,14 @@ export interface BundleDetail {
amount: string | null;
}

export async function getBundleDetail(bundleId: string): Promise<BundleDetail> {
export async function getBundleDetail(
ppPublicKey: string,
bundleId: string,
): Promise<BundleDetail> {
const res = await platformFetch(
`/dashboard/bundles/${encodeURIComponent(bundleId)}`,
`/providers/${encodeURIComponent(ppPublicKey)}/bundles/${
encodeURIComponent(bundleId)
}`,
);
if (!res.ok) throw new Error("Failed to fetch bundle detail");
const body = await res.json();
Expand All @@ -395,8 +404,10 @@ export async function listRecentBundles(
ppPublicKey: string,
limit: number,
): Promise<RecentBundleSummary[]> {
const qs = new URLSearchParams({ ppPublicKey, limit: String(limit) });
const res = await platformFetch(`/dashboard/bundles?${qs}`);
const qs = new URLSearchParams({ limit: String(limit) });
const res = await platformFetch(
`/providers/${encodeURIComponent(ppPublicKey)}/bundles?${qs}`,
);
if (!res.ok) throw new Error("Failed to list recent bundles");
const body = await res.json();
return (body.data as { bundles: RecentBundleSummary[] }).bundles;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/events-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* WebSocket client for /api/v1/events/ws on provider-platform.
* WebSocket client for /api/v1/providers/:ppPublicKey/events/ws on provider-platform.
*
* - URL derived from API_BASE_URL by swapping http(s):// → ws(s)://.
* - Auth via `Sec-WebSocket-Protocol: moonlight.events.v1, bearer.<JWT>`.
Expand Down Expand Up @@ -173,9 +173,9 @@ export class EventsClient {
return;
}
const base = wsUrlFromApiBase(API_BASE_URL);
const url = `${base}/events/ws?pp=${
const url = `${base}/providers/${
encodeURIComponent(this.opts.ppPublicKey)
}`;
}/events/ws`;
this.opts.onStatus?.("connecting");
const sock = new WebSocket(url, [SUBPROTOCOL, `bearer.${token}`]);
this.socket = sock;
Expand Down
15 changes: 14 additions & 1 deletion src/lib/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type RouteEntry = {
const routes: RouteEntry[] = [];
let cleanups: (() => void)[] = [];
let currentParams: Record<string, string> = {};
let currentQuery: URLSearchParams = new URLSearchParams();

function parsePattern(pattern: string): Omit<RouteEntry, "handler"> {
const segments = pattern.split("/").filter((s) => s.length > 0);
Expand Down Expand Up @@ -57,9 +58,21 @@ export function getRouteParams(): Record<string, string> {
return currentParams;
}

/**
* Returns the parsed query string from the current hash route.
* `#/foo/bar?x=1&y=2` → URLSearchParams of `x=1&y=2`.
*/
export function getRouteQuery(): URLSearchParams {
return currentQuery;
}

async function render(): Promise<void> {
const hash = globalThis.location.hash || "#/";
const path = hash.startsWith("#") ? hash.slice(1) : hash;
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
const qIdx = raw.indexOf("?");
const path = qIdx === -1 ? raw : raw.slice(0, qIdx);
const queryString = qIdx === -1 ? "" : raw.slice(qIdx + 1);
currentQuery = new URLSearchParams(queryString);
const pathSegments = path.split("/").filter((s) => s.length > 0);

let matched: { entry: RouteEntry; params: Record<string, string> } | null =
Expand Down
95 changes: 95 additions & 0 deletions src/lib/wallet-kyc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Wallet integration for the public KYC/KYB submission route.
*
* Isolation contract — this module:
* - Holds the connected address in MODULE-LOCAL state. No localStorage,
* sessionStorage, IndexedDB, or cookies for any artifact (address,
* signed challenge, derived data).
* - Never reads or writes the operator-auth keys (provider_admin_address,
* master_seed, console_token). Existing operator sessions on other routes
* are unaffected by KYC-route activity, and vice versa.
* - Refresh / navigation away purges all state. The user must reconnect.
*
* The Stellar Wallets Kit is a static SDK shared with operator-auth code, but
* the kit's own internal state (selected wallet, network) is ephemeral and
* does not persist KYC-relevant data. We call its `authModal()` and
* `signMessage()` directly; we do NOT call `setWallet()` (no caching).
*/
import { StellarWalletsKit } from "@creit-tech/stellar-wallets-kit/sdk";
import { Networks } from "@creit-tech/stellar-wallets-kit/types";
import { FreighterModule } from "@creit-tech/stellar-wallets-kit/modules/freighter";
import { STELLAR_NETWORK } from "./config.ts";

let kitInitialized = false;
let kycAddress: string | null = null;

function getWalletNetwork(): Networks {
switch (STELLAR_NETWORK) {
case "mainnet":
return Networks.PUBLIC;
case "standalone":
return Networks.STANDALONE;
default:
return Networks.TESTNET;
}
}

function getNetworkPassphrase(): string {
switch (STELLAR_NETWORK) {
case "mainnet":
return "Public Global Stellar Network ; September 2015";
case "standalone":
return "Standalone Network ; February 2017";
default:
return "Test SDF Network ; September 2015";
}
}

function ensureKitInit(): void {
if (kitInitialized) return;
StellarWalletsKit.init({
modules: [new FreighterModule()],
network: getWalletNetwork(),
});
kitInitialized = true;
}

export function getKycAddress(): string | null {
return kycAddress;
}

export function clearKycWallet(): void {
kycAddress = null;
}

/**
* Opens the wallet modal and stores the chosen address in module-local state.
*/
export async function connectKycWallet(): Promise<string> {
ensureKitInit();
const { address } = await StellarWalletsKit.authModal();
if (!address) throw new Error("Wallet connect cancelled");
kycAddress = address;
return address;
}

/**
* Signs the given challenge message (SEP-53) with the connected wallet.
* Returns the wallet's signedMessage string verbatim — the server side
* accepts hex or base64 and tries SEP-43, SEP-53, and raw fallbacks.
*/
export async function signKycMessage(message: string): Promise<string> {
ensureKitInit();
if (!kycAddress) throw new Error("Wallet not connected");
const result = await StellarWalletsKit.signMessage(message, {
address: kycAddress,
networkPassphrase: getNetworkPassphrase(),
});
if (
typeof result?.signedMessage !== "string" ||
result.signedMessage.length === 0
) {
throw new Error("Wallet returned an empty signature");
}
return result.signedMessage;
}
Loading
Loading