docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
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:
145
site-manager-api/src/cli/index.ts
Normal file
145
site-manager-api/src/cli/index.ts
Normal 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();
|
||||
157
site-manager-api/src/client/SiteManagerClient.ts
Normal file
157
site-manager-api/src/client/SiteManagerClient.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
44
site-manager-api/src/errors/SiteManagerErrors.ts
Normal file
44
site-manager-api/src/errors/SiteManagerErrors.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
25
site-manager-api/src/index.ts
Normal file
25
site-manager-api/src/index.ts
Normal 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';
|
||||
18
site-manager-api/src/services/DevicesService.ts
Normal file
18
site-manager-api/src/services/DevicesService.ts
Normal 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 : [];
|
||||
}
|
||||
}
|
||||
18
site-manager-api/src/services/HostsService.ts
Normal file
18
site-manager-api/src/services/HostsService.ts
Normal 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 : [];
|
||||
}
|
||||
}
|
||||
52
site-manager-api/src/services/MetricsService.ts
Normal file
52
site-manager-api/src/services/MetricsService.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
18
site-manager-api/src/services/SitesService.ts
Normal file
18
site-manager-api/src/services/SitesService.ts
Normal 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 : [];
|
||||
}
|
||||
}
|
||||
31
site-manager-api/src/types/api.ts
Normal file
31
site-manager-api/src/types/api.ts
Normal 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;
|
||||
}
|
||||
11
site-manager-api/src/types/devices.ts
Normal file
11
site-manager-api/src/types/devices.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Types for Devices endpoint
|
||||
*/
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
name?: string;
|
||||
model?: string;
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
11
site-manager-api/src/types/hosts.ts
Normal file
11
site-manager-api/src/types/hosts.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Types for Hosts endpoint
|
||||
*/
|
||||
|
||||
export interface Host {
|
||||
id: string;
|
||||
name?: string;
|
||||
hostname?: string;
|
||||
address?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
20
site-manager-api/src/types/metrics.ts
Normal file
20
site-manager-api/src/types/metrics.ts
Normal 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;
|
||||
}
|
||||
9
site-manager-api/src/types/sites.ts
Normal file
9
site-manager-api/src/types/sites.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Types for Sites endpoint
|
||||
*/
|
||||
|
||||
export interface Site {
|
||||
id: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
Reference in New Issue
Block a user