docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled

- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands
- CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround
- CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check
- NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere
- MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates
- LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-12 15:46:57 -08:00
parent cc8dcaf356
commit fbda1b4beb
5114 changed files with 498901 additions and 4567 deletions

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env node
/**
* Site Manager API CLI Tool
* Command-line interface for Site Manager Cloud API operations
*/
import { Command } from 'commander';
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { SiteManagerClient } from '../client/SiteManagerClient.js';
import { HostsService } from '../services/HostsService.js';
import { SitesService } from '../services/SitesService.js';
import { DevicesService } from '../services/DevicesService.js';
import { MetricsService } from '../services/MetricsService.js';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath: string): boolean {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const program = new Command();
program
.name('site-manager-cli')
.description('CLI tool for UniFi Site Manager Cloud API operations')
.version('1.0.0');
function createClient(): SiteManagerClient {
const apiKey = process.env.SITE_MANAGER_API_KEY;
if (!apiKey) {
throw new Error('SITE_MANAGER_API_KEY environment variable is required');
}
return new SiteManagerClient({
apiKey,
baseUrl: process.env.SITE_MANAGER_BASE_URL,
});
}
// Hosts commands
program
.command('hosts')
.description('List all hosts')
.action(async () => {
try {
const client = createClient();
const hostsService = new HostsService(client);
const hosts = await hostsService.listHosts();
console.log(JSON.stringify(hosts, null, 2));
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
// Sites commands
program
.command('sites')
.description('List all sites')
.action(async () => {
try {
const client = createClient();
const sitesService = new SitesService(client);
const sites = await sitesService.listSites();
console.log(JSON.stringify(sites, null, 2));
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
// Devices commands
program
.command('devices')
.description('List all devices')
.action(async () => {
try {
const client = createClient();
const devicesService = new DevicesService(client);
const devices = await devicesService.listDevices();
console.log(JSON.stringify(devices, null, 2));
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
// Metrics commands
program
.command('isp-metrics')
.description('Get ISP metrics')
.action(async () => {
try {
const client = createClient();
const metricsService = new MetricsService(client);
const metrics = await metricsService.getISPMetrics();
console.log(JSON.stringify(metrics, null, 2));
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
program
.command('sd-wan-configs')
.description('List SD-WAN configurations')
.action(async () => {
try {
const client = createClient();
const metricsService = new MetricsService(client);
const configs = await metricsService.listSDWANConfigs();
console.log(JSON.stringify(configs, null, 2));
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
program.parse();

View File

@@ -0,0 +1,157 @@
/**
* Site Manager API Client
* Cloud-based API for managing UniFi deployments at scale
* Documentation: https://developer.ui.com/site-manager-api/gettingstarted
*/
import {
SiteManagerApiError,
SiteManagerAuthenticationError,
SiteManagerNetworkError,
SiteManagerRateLimitError,
} from '../errors/SiteManagerErrors.js';
import {
ApiRequestOptions,
PaginatedResponse,
SiteManagerErrorResponse,
} from '../types/api.js';
export interface SiteManagerClientConfig {
apiKey: string;
baseUrl?: string; // Defaults to https://api.ui.com/v1
}
/**
* Client for interacting with Site Manager Cloud API
*/
export class SiteManagerClient {
private readonly baseUrl: string;
private readonly apiKey: string;
constructor(config: SiteManagerClientConfig) {
this.baseUrl = config.baseUrl || 'https://api.ui.com/v1';
this.apiKey = config.apiKey;
if (!this.apiKey) {
throw new Error('API key is required for Site Manager API');
}
}
/**
* Make a request to the Site Manager API
*/
async request<T = any>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
endpoint: string,
options: Omit<ApiRequestOptions, 'method'> = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
// Build query parameters
let fullUrl = url;
if (options.params && Object.keys(options.params).length > 0) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(options.params)) {
if (value !== undefined) {
searchParams.append(key, String(value));
}
}
fullUrl += `?${searchParams.toString()}`;
}
// Prepare headers
const headers: Record<string, string> = {
'X-API-KEY': this.apiKey,
'Accept': 'application/json',
...options.headers,
};
if (options.body && method !== 'GET') {
headers['Content-Type'] = 'application/json';
}
try {
const response = await fetch(fullUrl, {
method,
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
});
// Handle rate limiting
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new SiteManagerRateLimitError(
'Rate limit exceeded',
retryAfter ? parseInt(retryAfter, 10) : undefined,
await response.text().catch(() => undefined)
);
}
// Handle authentication errors
if (response.status === 401) {
const errorData = await response.json().catch(() => ({}));
throw new SiteManagerAuthenticationError(
'Authentication failed - check your API key',
errorData
);
}
// Handle other errors
if (!response.ok) {
let errorData: SiteManagerErrorResponse;
try {
const jsonData = await response.json() as unknown;
errorData = jsonData as SiteManagerErrorResponse;
} catch {
errorData = { message: await response.text() };
}
throw new SiteManagerApiError(
errorData.message || errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
// Parse response
const data = await response.json();
return data as T;
} catch (error) {
if (error instanceof SiteManagerApiError) {
throw error;
}
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new SiteManagerNetworkError(
`Failed to connect to Site Manager API: ${error.message}`,
error as Error
);
}
throw new SiteManagerApiError(
`Request failed: ${error instanceof Error ? error.message : String(error)}`,
undefined,
error
);
}
}
/**
* Make a paginated request
*/
async requestPaginated<T = any>(
method: 'GET',
endpoint: string,
options: Omit<ApiRequestOptions, 'method'> = {}
): Promise<PaginatedResponse<T>> {
const result = await this.request<PaginatedResponse<T>>(method, endpoint, options);
return result;
}
/**
* Get API key (for testing/debugging)
*/
getApiKey(): string {
return this.apiKey;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Custom error classes for Site Manager API operations
*/
export class SiteManagerApiError extends Error {
constructor(
message: string,
public statusCode?: number,
public response?: any
) {
super(message);
this.name = 'SiteManagerApiError';
Object.setPrototypeOf(this, SiteManagerApiError.prototype);
}
}
export class SiteManagerAuthenticationError extends SiteManagerApiError {
constructor(message: string = 'Authentication failed', response?: any) {
super(message, 401, response);
this.name = 'SiteManagerAuthenticationError';
Object.setPrototypeOf(this, SiteManagerAuthenticationError.prototype);
}
}
export class SiteManagerNetworkError extends SiteManagerApiError {
constructor(message: string = 'Network error', cause?: Error) {
super(message);
this.name = 'SiteManagerNetworkError';
this.cause = cause;
Object.setPrototypeOf(this, SiteManagerNetworkError.prototype);
}
}
export class SiteManagerRateLimitError extends SiteManagerApiError {
constructor(
message: string = 'Rate limit exceeded',
public retryAfter?: number,
response?: any
) {
super(message, 429, response);
this.name = 'SiteManagerRateLimitError';
Object.setPrototypeOf(this, SiteManagerRateLimitError.prototype);
}
}

View File

@@ -0,0 +1,25 @@
/**
* Site Manager API Library
* Export all public APIs
*/
export { SiteManagerClient } from './client/SiteManagerClient.js';
export type { SiteManagerClientConfig } from './client/SiteManagerClient.js';
export { HostsService } from './services/HostsService.js';
export { SitesService } from './services/SitesService.js';
export { DevicesService } from './services/DevicesService.js';
export { MetricsService } from './services/MetricsService.js';
export {
SiteManagerApiError,
SiteManagerAuthenticationError,
SiteManagerNetworkError,
SiteManagerRateLimitError,
} from './errors/SiteManagerErrors.js';
export type { Host } from './types/hosts.js';
export type { Site } from './types/sites.js';
export type { Device } from './types/devices.js';
export type { ISPMetric, SDWANConfig, SDWANConfigStatus } from './types/metrics.js';
export type { ApiResponse, PaginatedResponse } from './types/api.js';

View File

@@ -0,0 +1,18 @@
/**
* Service for managing devices
*/
import { SiteManagerClient } from '../client/SiteManagerClient.js';
import { Device } from '../types/devices.js';
export class DevicesService {
constructor(private client: SiteManagerClient) {}
/**
* List all devices
*/
async listDevices(): Promise<Device[]> {
const response = await this.client.requestPaginated<Device>('GET', '/devices');
return Array.isArray(response.data) ? response.data : [];
}
}

View File

@@ -0,0 +1,18 @@
/**
* Service for managing hosts
*/
import { SiteManagerClient } from '../client/SiteManagerClient.js';
import { Host } from '../types/hosts.js';
export class HostsService {
constructor(private client: SiteManagerClient) {}
/**
* List all hosts
*/
async listHosts(): Promise<Host[]> {
const response = await this.client.requestPaginated<Host>('GET', '/hosts');
return Array.isArray(response.data) ? response.data : [];
}
}

View File

@@ -0,0 +1,52 @@
/**
* Service for ISP metrics and SD-WAN configurations
*/
import { SiteManagerClient } from '../client/SiteManagerClient.js';
import { ISPMetric, SDWANConfig, SDWANConfigStatus } from '../types/metrics.js';
export class MetricsService {
constructor(private client: SiteManagerClient) {}
/**
* Get ISP metrics
*/
async getISPMetrics(params?: Record<string, any>): Promise<ISPMetric[]> {
const response = await this.client.requestPaginated<ISPMetric>('GET', '/isp-metrics', {
params,
});
return Array.isArray(response.data) ? response.data : [];
}
/**
* Query ISP metrics with filters
*/
async queryISPMetrics(params?: Record<string, any>): Promise<ISPMetric[]> {
const response = await this.client.requestPaginated<ISPMetric>('GET', '/isp-metrics/query', {
params,
});
return Array.isArray(response.data) ? response.data : [];
}
/**
* List SD-WAN configurations
*/
async listSDWANConfigs(): Promise<SDWANConfig[]> {
const response = await this.client.requestPaginated<SDWANConfig>('GET', '/sd-wan-configs');
return Array.isArray(response.data) ? response.data : [];
}
/**
* Get SD-WAN config by ID
*/
async getSDWANConfig(id: string): Promise<SDWANConfig> {
return await this.client.request<SDWANConfig>('GET', `/sd-wan-configs/${id}`);
}
/**
* Get SD-WAN config status
*/
async getSDWANConfigStatus(id: string): Promise<SDWANConfigStatus> {
return await this.client.request<SDWANConfigStatus>('GET', `/sd-wan-configs/${id}/status`);
}
}

View File

@@ -0,0 +1,18 @@
/**
* Service for managing sites
*/
import { SiteManagerClient } from '../client/SiteManagerClient.js';
import { Site } from '../types/sites.js';
export class SitesService {
constructor(private client: SiteManagerClient) {}
/**
* List all sites
*/
async listSites(): Promise<Site[]> {
const response = await this.client.requestPaginated<Site>('GET', '/sites');
return Array.isArray(response.data) ? response.data : [];
}
}

View File

@@ -0,0 +1,31 @@
/**
* Core API types for Site Manager API
*/
export interface ApiRequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
params?: Record<string, string | number | boolean | undefined>;
body?: any;
headers?: Record<string, string>;
}
export interface ApiResponse<T = any> {
data: T;
[key: string]: any;
}
export interface PaginatedResponse<T = any> {
data: T[];
offset?: number;
limit?: number;
count?: number;
totalCount?: number;
[key: string]: any;
}
export interface SiteManagerErrorResponse {
error?: string;
message?: string;
code?: string;
[key: string]: any;
}

View File

@@ -0,0 +1,11 @@
/**
* Types for Devices endpoint
*/
export interface Device {
id: string;
name?: string;
model?: string;
type?: string;
[key: string]: any;
}

View File

@@ -0,0 +1,11 @@
/**
* Types for Hosts endpoint
*/
export interface Host {
id: string;
name?: string;
hostname?: string;
address?: string;
[key: string]: any;
}

View File

@@ -0,0 +1,20 @@
/**
* Types for ISP Metrics and SD-WAN endpoints
*/
export interface ISPMetric {
timestamp?: string;
[key: string]: any;
}
export interface SDWANConfig {
id: string;
name?: string;
[key: string]: any;
}
export interface SDWANConfigStatus {
id: string;
status?: string;
[key: string]: any;
}

View File

@@ -0,0 +1,9 @@
/**
* Types for Sites endpoint
*/
export interface Site {
id: string;
name?: string;
[key: string]: any;
}