Initial AS213905 SDK release
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# JavaScript / TypeScript SDK
|
||||
|
||||
```bash
|
||||
npm install git+https://git.datacoria.com/Phylex/as213905-sdk.git#javascript
|
||||
```
|
||||
|
||||
```js
|
||||
import {AS213905} from '@as213905/sdk';
|
||||
const client = new AS213905({apiKey: process.env.AS213905_API_KEY});
|
||||
console.log(await client.traffic({days: 7}));
|
||||
```
|
||||
|
||||
The package has no dependencies, works in Node 18+ and modern browsers, and
|
||||
ships TypeScript declarations.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@as213905/sdk",
|
||||
"version": "1.1.0",
|
||||
"description": "Official AS213905 customer API client",
|
||||
"type": "module",
|
||||
"main": "./src/index.js",
|
||||
"types": "./src/index.d.ts",
|
||||
"exports": {".": {"types": "./src/index.d.ts", "import": "./src/index.js"}},
|
||||
"files": ["src", "README.md"],
|
||||
"scripts": {"test": "node --test", "check": "node --check src/index.js"},
|
||||
"engines": {"node": ">=18"},
|
||||
"license": "MIT",
|
||||
"repository": {"type": "git", "url": "https://git.datacoria.com/Phylex/as213905-sdk.git", "directory": "javascript"}
|
||||
}
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
export interface ClientOptions { apiKey: string; baseUrl?: string; timeout?: number; fetch?: typeof fetch }
|
||||
export interface RequestOptions { signal?: AbortSignal }
|
||||
export interface Organization { id: string; asn: number; name: string; status: string; commit_kbps: number }
|
||||
export interface Invoice { id: string; number: string; status: string; currency: string; total_cents: number; period_from?: string|null; period_to?: string|null; issued_at?: string|null; due_at?: string|null }
|
||||
export interface Service { id: string; kind: string; label: string; status: string; monthly_cents: number; next_due_on?: string|null; controllable?: boolean; location?: string; configuration?: Record<string, unknown> }
|
||||
export interface Period { from: string; to: string; label: string }
|
||||
export interface Traffic { p95_kbps: number; commit_kbps?: number; over_commit?: boolean; total_bytes: number; total_gb: number; period: Period }
|
||||
export interface Tunnel { id: string; status: string; label?: string; location: string; peer_endpoint: string; ifname?: string; inner_v4?: string; inner_v6?: string; announce?: boolean }
|
||||
export interface CreateTunnel { peer_endpoint: string; location: string; label?: string; announce?: boolean }
|
||||
export interface GeofeedRecord { id?: string; prefix: string; country_code: string; region_code?: string; city?: string; postal_code?: string }
|
||||
export interface Geofeed { id: string; name: string; slug: string; record_count: number; created_at: string; updated_at: string; records?: GeofeedRecord[] }
|
||||
export interface GeofeedList { geofeeds: Geofeed[]; limit: number }
|
||||
export class APIError extends Error { status: number; code: string; requiredScope?: string; retryAfter?: number }
|
||||
export class AS213905 {
|
||||
constructor(options: ClientOptions);
|
||||
organization(options?: RequestOptions): Promise<Organization>;
|
||||
invoices(options?: RequestOptions): Promise<Invoice[]>;
|
||||
services(options?: RequestOptions): Promise<Service[]>;
|
||||
service(id: string, options?: RequestOptions): Promise<Service>;
|
||||
serviceMetrics(id: string, options?: RequestOptions & {target?: string; range?: string}): Promise<{metric:string;target:string;range:string;points:Array<{t:string;v?:number}>}>;
|
||||
powerService(id: string, action: 'start'|'stop'|'restart', options?: RequestOptions): Promise<{status:string;action:string}>;
|
||||
traffic(options?: RequestOptions & {days?:number;from?:string;to?:string}): Promise<Traffic>;
|
||||
tunnels(options?: RequestOptions): Promise<Tunnel[]>;
|
||||
createTunnel(input: CreateTunnel, options?: RequestOptions): Promise<Tunnel>;
|
||||
deleteTunnel(id: string, options?: RequestOptions): Promise<unknown>;
|
||||
tunnelTraffic(id:string, options?:RequestOptions & {days?:number;from?:string;to?:string}):Promise<Traffic & {tunnel:Tunnel}>;
|
||||
geofeeds(organizationId:string,options?:RequestOptions):Promise<GeofeedList>;
|
||||
createGeofeed(organizationId:string,name:string,options?:RequestOptions):Promise<GeofeedList>;
|
||||
geofeed(organizationId:string,id:string,options?:RequestOptions):Promise<Geofeed>;
|
||||
renameGeofeed(organizationId:string,id:string,name:string,options?:RequestOptions):Promise<unknown>;
|
||||
deleteGeofeed(organizationId:string,id:string,options?:RequestOptions):Promise<unknown>;
|
||||
replaceGeofeedRecords(organizationId:string,id:string,records:GeofeedRecord[],options?:RequestOptions):Promise<unknown>;
|
||||
rotateGeofeedUrl(organizationId:string,id:string,options?:RequestOptions):Promise<GeofeedList>;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export class APIError extends Error {
|
||||
constructor(status, code, message = '', details = {}) {
|
||||
super(`AS213905 API error ${status} (${code})${message ? `: ${message}` : ''}`);
|
||||
this.name = 'APIError'; this.status = status; this.code = code;
|
||||
this.requiredScope = details.required_scope; this.retryAfter = details.retry_after;
|
||||
}
|
||||
}
|
||||
|
||||
export class AS213905 {
|
||||
constructor({apiKey, baseUrl = 'https://as213905.com', timeout = 30000, fetch: fetchImpl = globalThis.fetch} = {}) {
|
||||
if (!apiKey) throw new TypeError('apiKey is required');
|
||||
if (typeof fetchImpl !== 'function') throw new TypeError('fetch implementation is required');
|
||||
this.apiKey = apiKey; this.baseUrl = baseUrl.replace(/\/$/, ''); this.timeout = timeout; this.fetch = fetchImpl;
|
||||
}
|
||||
async request(method, path, {body, organizationId, signal} = {}) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(new Error('request timed out')), this.timeout);
|
||||
if (signal) signal.addEventListener('abort', () => controller.abort(signal.reason), {once: true});
|
||||
try {
|
||||
const response = await this.fetch(this.baseUrl + path, {
|
||||
method, signal: controller.signal,
|
||||
headers: {Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', ...(body === undefined ? {} : {'Content-Type': 'application/json'}), ...(organizationId ? {'X-Organization-ID': organizationId} : {})},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
const value = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new APIError(response.status, value.error || 'http_error', value.message || '', {...value, retry_after: value.retry_after || Number(response.headers.get('Retry-After')) || undefined});
|
||||
return value;
|
||||
} finally { clearTimeout(timer); }
|
||||
}
|
||||
static query(values) { const q = new URLSearchParams(Object.entries(values).filter(([,v]) => v !== undefined && v !== null)); return q.size ? `?${q}` : ''; }
|
||||
organization(options) { return this.request('GET', '/api/v1/organization', options).then(v => v.organization); }
|
||||
invoices(options) { return this.request('GET', '/api/v1/invoices', options).then(v => v.invoices); }
|
||||
services(options) { return this.request('GET', '/api/v1/services', options).then(v => v.services); }
|
||||
service(id, options) { return this.request('GET', `/api/v1/services/${encodeURIComponent(id)}`, options).then(v => v.service); }
|
||||
serviceMetrics(id, {target, range, ...options} = {}) { return this.request('GET', `/api/v1/services/${encodeURIComponent(id)}/metrics${AS213905.query({target, range})}`, options); }
|
||||
powerService(id, action, options = {}) { return this.request('POST', `/api/v1/services/${encodeURIComponent(id)}/power`, {...options, body: {action}}); }
|
||||
traffic({days, from, to, ...options} = {}) { return this.request('GET', `/api/v1/traffic${AS213905.query({days, from, to})}`, options); }
|
||||
tunnels(options) { return this.request('GET', '/api/v1/tunnels', options).then(v => v.tunnels); }
|
||||
createTunnel(input, options = {}) { return this.request('POST', '/api/v1/tunnels', {...options, body: input}).then(v => v.tunnel); }
|
||||
deleteTunnel(id, options) { return this.request('DELETE', `/api/v1/tunnels/${encodeURIComponent(id)}`, options); }
|
||||
tunnelTraffic(id, {days, from, to, ...options} = {}) { return this.request('GET', `/api/v1/tunnels/${encodeURIComponent(id)}/traffic${AS213905.query({days, from, to})}`, options); }
|
||||
geofeeds(organizationId, options = {}) { return this.request('GET', '/api/v1/geofeeds', {...options, organizationId}); }
|
||||
createGeofeed(organizationId, name, options = {}) { return this.request('POST', '/api/v1/geofeeds', {...options, organizationId, body: {name}}); }
|
||||
geofeed(organizationId, id, options = {}) { return this.request('GET', `/api/v1/geofeeds/${encodeURIComponent(id)}`, {...options, organizationId}).then(v => v.geofeed); }
|
||||
renameGeofeed(organizationId, id, name, options = {}) { return this.request('PATCH', `/api/v1/geofeeds/${encodeURIComponent(id)}`, {...options, organizationId, body: {name}}); }
|
||||
deleteGeofeed(organizationId, id, options = {}) { return this.request('DELETE', `/api/v1/geofeeds/${encodeURIComponent(id)}`, {...options, organizationId}); }
|
||||
replaceGeofeedRecords(organizationId, id, records, options = {}) { return this.request('PUT', `/api/v1/geofeeds/${encodeURIComponent(id)}/records`, {...options, organizationId, body: {records}}); }
|
||||
rotateGeofeedUrl(organizationId, id, options = {}) { return this.request('POST', `/api/v1/geofeeds/${encodeURIComponent(id)}/rotate`, {...options, organizationId}); }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {AS213905, APIError} from '../src/index.js';
|
||||
|
||||
test('sets bearer authentication and parses organization', async () => {
|
||||
const client = new AS213905({apiKey:'secret',fetch:async (url,init)=>{assert.equal(init.headers.Authorization,'Bearer secret');return new Response(JSON.stringify({organization:{asn:213905}}),{status:200,headers:{'content-type':'application/json'}})}});
|
||||
assert.equal((await client.organization()).asn,213905);
|
||||
});
|
||||
|
||||
test('throws a structured APIError', async () => {
|
||||
const client = new AS213905({apiKey:'secret',fetch:async()=>new Response(JSON.stringify({error:'scope_required',message:'need traffic:read'}),{status:403})});
|
||||
await assert.rejects(client.traffic(),error=>error instanceof APIError&&error.status===403&&error.code==='scope_required');
|
||||
});
|
||||
Reference in New Issue
Block a user