@dszp/netsapiens-lib 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/nsClient.d.ts +5 -1
- package/dist/nsClient.js +5 -1
- package/dist/nsWriteClient.d.ts +45 -0
- package/dist/nsWriteClient.js +81 -0
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { THEMES, DEFAULT_LIGHT_THEME, DEFAULT_DARK_THEME, NODE_LIGHT, NODE_DARK,
|
|
|
16
16
|
export { renderGalleryHtml, renderFlowCards, renderFlowCard, mermaidBootstrap, flowAnchorId, type GalleryOptions, type CardOptions, } from './html.js';
|
|
17
17
|
export { resolveSvgSize, rasterizerScript } from './raster.js';
|
|
18
18
|
export { NsClient, NsApiError, assertBareServer, fetchDomainSnapshot, listDomains, asArray, type NsClientConfig, type FetchSnapshotOptions } from './nsClient.js';
|
|
19
|
+
export { NsWriteClient, type NsWriteClientConfig } from './nsWriteClient.js';
|
|
19
20
|
export { verify, validateJwtFormat, extractContext, assertClaims, verifyHs256Signature, normalizeToken, tokenKey, MemoryVerdictCache, type JwtVerdict, type JwtContext, type ClaimExpectations, type VerdictCache, type VerifyOptions, type FormatResult, } from './jwt.js';
|
|
20
21
|
export { type CallSensitivity, needsFreshAuth, SENSITIVITY_NOTE } from './sensitivity.js';
|
|
21
22
|
export { toPrincipal, parseOperator, isResellerScope, isAdminScope, type Principal, type Operator, type Scope, } from './principal.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { THEMES, DEFAULT_LIGHT_THEME, DEFAULT_DARK_THEME, NODE_LIGHT, NODE_DARK,
|
|
|
15
15
|
export { renderGalleryHtml, renderFlowCards, renderFlowCard, mermaidBootstrap, flowAnchorId, } from './html.js';
|
|
16
16
|
export { resolveSvgSize, rasterizerScript } from './raster.js';
|
|
17
17
|
export { NsClient, NsApiError, assertBareServer, fetchDomainSnapshot, listDomains, asArray } from './nsClient.js';
|
|
18
|
+
export { NsWriteClient } from './nsWriteClient.js';
|
|
18
19
|
export { verify, validateJwtFormat, extractContext, assertClaims, verifyHs256Signature, normalizeToken, tokenKey, MemoryVerdictCache, } from './jwt.js';
|
|
19
20
|
export { needsFreshAuth, SENSITIVITY_NOTE } from './sensitivity.js';
|
|
20
21
|
export { toPrincipal, parseOperator, isResellerScope, isAdminScope, } from './principal.js';
|
package/dist/nsClient.d.ts
CHANGED
|
@@ -14,7 +14,11 @@ export declare class NsApiError extends Error {
|
|
|
14
14
|
readonly status: number;
|
|
15
15
|
readonly path: string;
|
|
16
16
|
readonly body: unknown;
|
|
17
|
-
|
|
17
|
+
/** HTTP method — set by the write client; optional so read-client call sites stay unchanged. */
|
|
18
|
+
readonly method?: string | undefined;
|
|
19
|
+
constructor(message: string, status: number, path: string, body: unknown,
|
|
20
|
+
/** HTTP method — set by the write client; optional so read-client call sites stay unchanged. */
|
|
21
|
+
method?: string | undefined);
|
|
18
22
|
}
|
|
19
23
|
export interface NsClientConfig {
|
|
20
24
|
/** API host, e.g. "api.example.com". Base URL becomes https://{server}/ns-api/v2. */
|
package/dist/nsClient.js
CHANGED
|
@@ -13,11 +13,15 @@ export class NsApiError extends Error {
|
|
|
13
13
|
status;
|
|
14
14
|
path;
|
|
15
15
|
body;
|
|
16
|
-
|
|
16
|
+
method;
|
|
17
|
+
constructor(message, status, path, body,
|
|
18
|
+
/** HTTP method — set by the write client; optional so read-client call sites stay unchanged. */
|
|
19
|
+
method) {
|
|
17
20
|
super(message);
|
|
18
21
|
this.status = status;
|
|
19
22
|
this.path = path;
|
|
20
23
|
this.body = body;
|
|
24
|
+
this.method = method;
|
|
21
25
|
this.name = 'NsApiError';
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable NetSapiens API v2 WRITE client — the separate, explicitly-imported write surface the read-only
|
|
3
|
+
* `NsClient` deliberately does not have (a consumer holds `NsClient` precisely to know it cannot write).
|
|
4
|
+
* This realizes the lib's planned split: read and write are two classes; only this one mutates. Node-free
|
|
5
|
+
* (fetch/URL/crypto only), so it runs unchanged in a Cloudflare Worker.
|
|
6
|
+
*
|
|
7
|
+
* Starts with the device methods the portal's Ringotel activation needs (create/get/delete), over a
|
|
8
|
+
* generic post/put/delete core, and is meant to GROW into the full NS write surface (users, DIDs, …) —
|
|
9
|
+
* porting the endpoint/body shapes from the onboarding tool's resource defs as they're needed.
|
|
10
|
+
*
|
|
11
|
+
* Like the onboarding client, POST/PUT inject `synchronous: 'yes'` so a create returns 200 + the created
|
|
12
|
+
* resource inline (with server-generated fields — e.g. a device's `device-sip-registration-password`)
|
|
13
|
+
* instead of a 202 with replication lag. Shares the read client's SSRF guard and `NsApiError`.
|
|
14
|
+
*/
|
|
15
|
+
import type { Rec } from './model.js';
|
|
16
|
+
export interface NsWriteClientConfig {
|
|
17
|
+
/** API host, e.g. "api.example.com". Base URL becomes https://{server}/ns-api/v2. */
|
|
18
|
+
server: string;
|
|
19
|
+
/** Bearer token (an API key with write scope). */
|
|
20
|
+
token: string;
|
|
21
|
+
/** Injectable for tests / non-global fetch. */
|
|
22
|
+
fetchImpl?: typeof fetch;
|
|
23
|
+
}
|
|
24
|
+
export declare class NsWriteClient {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(cfg: NsWriteClientConfig);
|
|
27
|
+
get<T = unknown>(path: string, query?: Record<string, string | number>): Promise<T>;
|
|
28
|
+
/** POST with `synchronous:'yes'` injected → 200 + created resource inline. */
|
|
29
|
+
post<T = unknown>(path: string, body: Rec): Promise<T>;
|
|
30
|
+
/** PUT with `synchronous:'yes'` injected. */
|
|
31
|
+
put<T = unknown>(path: string, body: Rec): Promise<T>;
|
|
32
|
+
delete<T = unknown>(path: string): Promise<T>;
|
|
33
|
+
/** List a user's devices (normalized to an array). */
|
|
34
|
+
getDevices(domain: string, user: string): Promise<Rec[]>;
|
|
35
|
+
/** Read one device (e.g. to fetch its `device-sip-registration-password`). */
|
|
36
|
+
getDevice(domain: string, user: string, device: string): Promise<Rec>;
|
|
37
|
+
/**
|
|
38
|
+
* Create a device (softphone when named `<ext><suffix>`, e.g. `100r`). NS auto-generates the SIP
|
|
39
|
+
* password when unset; with `synchronous:'yes'` it comes back inline in the response. `extra` allows
|
|
40
|
+
* optional fields (e.g. an emergency caller-id).
|
|
41
|
+
*/
|
|
42
|
+
createDevice(domain: string, user: string, device: string, extra?: Rec): Promise<Rec>;
|
|
43
|
+
/** Delete a device. */
|
|
44
|
+
deleteDevice(domain: string, user: string, device: string): Promise<Rec>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NsApiError, assertBareServer, asArray } from './nsClient.js';
|
|
2
|
+
const enc = encodeURIComponent;
|
|
3
|
+
export class NsWriteClient {
|
|
4
|
+
#baseUrl;
|
|
5
|
+
#token;
|
|
6
|
+
#fetchImpl;
|
|
7
|
+
constructor(cfg) {
|
|
8
|
+
this.#baseUrl = `https://${assertBareServer(cfg.server)}/ns-api/v2`;
|
|
9
|
+
this.#token = cfg.token;
|
|
10
|
+
this.#fetchImpl = cfg.fetchImpl ?? fetch;
|
|
11
|
+
}
|
|
12
|
+
// ── generic verbs (the growth surface) ──────────────────────────────────────
|
|
13
|
+
get(path, query) {
|
|
14
|
+
return this.#request('GET', path, undefined, query);
|
|
15
|
+
}
|
|
16
|
+
/** POST with `synchronous:'yes'` injected → 200 + created resource inline. */
|
|
17
|
+
post(path, body) {
|
|
18
|
+
return this.#request('POST', path, { synchronous: 'yes', ...body });
|
|
19
|
+
}
|
|
20
|
+
/** PUT with `synchronous:'yes'` injected. */
|
|
21
|
+
put(path, body) {
|
|
22
|
+
return this.#request('PUT', path, { synchronous: 'yes', ...body });
|
|
23
|
+
}
|
|
24
|
+
delete(path) {
|
|
25
|
+
return this.#request('DELETE', path);
|
|
26
|
+
}
|
|
27
|
+
// ── typed device helpers ────────────────────────────────────────────────────
|
|
28
|
+
/** List a user's devices (normalized to an array). */
|
|
29
|
+
getDevices(domain, user) {
|
|
30
|
+
return this.get(`/domains/${enc(domain)}/users/${enc(user)}/devices`).then(asArray);
|
|
31
|
+
}
|
|
32
|
+
/** Read one device (e.g. to fetch its `device-sip-registration-password`). */
|
|
33
|
+
getDevice(domain, user, device) {
|
|
34
|
+
return this.get(`/domains/${enc(domain)}/users/${enc(user)}/devices/${enc(device)}`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a device (softphone when named `<ext><suffix>`, e.g. `100r`). NS auto-generates the SIP
|
|
38
|
+
* password when unset; with `synchronous:'yes'` it comes back inline in the response. `extra` allows
|
|
39
|
+
* optional fields (e.g. an emergency caller-id).
|
|
40
|
+
*/
|
|
41
|
+
createDevice(domain, user, device, extra = {}) {
|
|
42
|
+
return this.post(`/domains/${enc(domain)}/users/${enc(user)}/devices`, { device, ...extra });
|
|
43
|
+
}
|
|
44
|
+
/** Delete a device. */
|
|
45
|
+
deleteDevice(domain, user, device) {
|
|
46
|
+
return this.delete(`/domains/${enc(domain)}/users/${enc(user)}/devices/${enc(device)}`);
|
|
47
|
+
}
|
|
48
|
+
async #request(method, path, body, query) {
|
|
49
|
+
const url = new URL(this.#baseUrl + path);
|
|
50
|
+
for (const [k, v] of Object.entries(query ?? {}))
|
|
51
|
+
url.searchParams.set(k, String(v));
|
|
52
|
+
// Call via a local, NOT `this.#fetchImpl(...)`: invoking the global fetch as a method of this
|
|
53
|
+
// instance throws "Illegal invocation" in workerd (the global fetch requires a global `this`).
|
|
54
|
+
const doFetch = this.#fetchImpl;
|
|
55
|
+
const res = await doFetch(url.toString(), {
|
|
56
|
+
method,
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${this.#token}`,
|
|
59
|
+
Accept: 'application/json',
|
|
60
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
61
|
+
},
|
|
62
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
63
|
+
});
|
|
64
|
+
const text = await res.text();
|
|
65
|
+
let parsed = text;
|
|
66
|
+
if (text) {
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* some endpoints return empty / plain bodies */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const detail = (typeof parsed === 'object' && parsed !== null ? JSON.stringify(parsed) : String(parsed)).slice(0, 500);
|
|
76
|
+
const hint = res.status === 401 ? ' (token expired/invalid or domain out of scope)' : res.status === 403 ? ' (token lacks permission)' : '';
|
|
77
|
+
throw new NsApiError(`${method} ${path} → ${res.status}${hint}: ${detail}`, res.status, path, parsed, method);
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dszp/netsapiens-lib",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Portable, Node-free NetSapiens toolkit: read-only API client, JWT (ns_t) validation, and a snapshot -> FlowGraph -> Mermaid call-flow resolver/renderer. Runs unchanged in a Cloudflare Worker, Node, or the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,9 +50,10 @@
|
|
|
50
50
|
"//prepublishOnly": "Publish-only build with sourcemaps OFF. The `files` globs exclude dist/**/*.map on purpose (they point at src/, which does not ship), but tsc still emits a //# sourceMappingURL pointer into every .js/.d.ts -- so consumers' devtools 404 chasing maps that were never published. Dropping the pointer at publish time is what the exclusion always meant. A normal `pnpm build` keeps maps for link: consumers.",
|
|
51
51
|
"prepublishOnly": "tsc -p tsconfig.json --sourceMap false --declarationMap false",
|
|
52
52
|
"//test": "The offline suite — green on a fresh clone with no credentials and no fixtures. test:ns is NOT included: it needs a domain snapshot that (correctly) isn't in the repo.",
|
|
53
|
-
"test": "pnpm run test:jwt && pnpm run test:principal && pnpm run test:resolver && pnpm run test:raster",
|
|
53
|
+
"test": "pnpm run test:jwt && pnpm run test:principal && pnpm run test:resolver && pnpm run test:raster && pnpm run test:nswrite",
|
|
54
54
|
"test:jwt": "tsx src/jwt.selftest.ts",
|
|
55
55
|
"test:ns": "tsx src/nsClient.selftest.ts",
|
|
56
|
+
"test:nswrite": "tsx src/nsWriteClient.selftest.ts",
|
|
56
57
|
"test:principal": "tsx src/principal.selftest.ts",
|
|
57
58
|
"test:resolver": "tsx src/resolver.selftest.ts",
|
|
58
59
|
"test:raster": "tsx src/raster.selftest.ts"
|