@fonoster/sipnet 0.30.0 → 0.31.1

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.
@@ -1,5 +1,11 @@
1
1
  import { ClientOptions } from "../types";
2
- declare function buildService(clientOptions: ClientOptions): {
2
+ /**
3
+ * `namesRoot` (e.g. `sip.example.com`) makes every domain one label under it;
4
+ * leave it out to accept any domain.
5
+ */
6
+ declare function buildService(clientOptions: ClientOptions, options?: {
7
+ namesRoot?: string;
8
+ }): {
3
9
  definition: {
4
10
  serviceName: string;
5
11
  pckg: string;
@@ -24,8 +24,13 @@ exports.buildService = buildService;
24
24
  */
25
25
  const sdk_1 = __importDefault(require("@routr/sdk"));
26
26
  const checkRefs_1 = require("../resources/checkRefs");
27
+ const sipNames_1 = require("../resources/sipNames");
27
28
  const operations_1 = require("./operations");
28
- function buildService(clientOptions) {
29
+ /**
30
+ * `namesRoot` (e.g. `sip.example.com`) makes every domain one label under it;
31
+ * leave it out to accept any domain.
32
+ */
33
+ function buildService(clientOptions, options = {}) {
29
34
  const client = new sdk_1.default.Domains(clientOptions);
30
35
  const checkRefs = (0, checkRefs_1.createCheckRefs)((0, checkRefs_1.createRefLookups)(clientOptions));
31
36
  return {
@@ -36,8 +41,8 @@ function buildService(clientOptions) {
36
41
  proto: "domains.proto"
37
42
  },
38
43
  handlers: {
39
- createDomain: (0, operations_1.createDomain)(client, checkRefs),
40
- updateDomain: (0, operations_1.updateDomain)(client, checkRefs),
44
+ createDomain: (0, sipNames_1.withSipNameCheck)((0, operations_1.createDomain)(client, checkRefs), "domainUri", "domain", options.namesRoot),
45
+ updateDomain: (0, sipNames_1.withSipNameCheck)((0, operations_1.updateDomain)(client, checkRefs), "domainUri", "domain", options.namesRoot),
41
46
  getDomain: (0, operations_1.getDomain)(client),
42
47
  listDomains: (0, operations_1.listDomains)(client),
43
48
  deleteDomain: (0, operations_1.deleteDomain)(client)
@@ -19,7 +19,7 @@
19
19
  import { NumberPreconditionsCheck } from "@fonoster/common";
20
20
  import { ClientOptions } from "../types";
21
21
  import { NumberRouting } from "./engineRouting";
22
- /** `routing` picks the peer app-linked numbers point at (default: the media-node peer). */
22
+ /** `routing` picks the peer app-linked numbers point at (default: the roomr peer). */
23
23
  declare function buildService(clientOptions: ClientOptions, checkNumberPreconditions: NumberPreconditionsCheck, routing?: NumberRouting): {
24
24
  definition: {
25
25
  serviceName: string;
@@ -11,7 +11,7 @@ const getNumber_1 = require("./getNumber");
11
11
  const listNumbers_1 = require("./listNumbers");
12
12
  const operations_1 = require("./operations");
13
13
  const updateNumber_1 = require("./updateNumber");
14
- /** `routing` picks the peer app-linked numbers point at (default: the media-node peer). */
14
+ /** `routing` picks the peer app-linked numbers point at (default: the roomr peer). */
15
15
  function buildService(clientOptions, checkNumberPreconditions, routing) {
16
16
  const client = new sdk_1.default.Numbers(clientOptions);
17
17
  const checkRefs = (0, checkRefs_1.createCheckRefs)((0, checkRefs_1.createRefLookups)(clientOptions));
@@ -1,6 +1,6 @@
1
1
  import { CreateNumberRequest, UpdateNumberRequest } from "@fonoster/types";
2
2
  /**
3
- * `appPeerAor` is the peer an app-linked number routes to: the media-node peer by default (see
3
+ * `appPeerAor` is the peer an app-linked number routes to: the roomr peer by default (see
4
4
  * engineRouting.ts).
5
5
  */
6
6
  declare function convertToRoutrNumber(number: CreateNumberRequest, accessKeyId: string, appPeerAor?: string): {
@@ -22,7 +22,7 @@ exports.convertToRoutrNumberUpdate = convertToRoutrNumberUpdate;
22
22
  */
23
23
  const common_1 = require("@fonoster/common");
24
24
  /**
25
- * `appPeerAor` is the peer an app-linked number routes to: the media-node peer by default (see
25
+ * `appPeerAor` is the peer an app-linked number routes to: the roomr peer by default (see
26
26
  * engineRouting.ts).
27
27
  */
28
28
  function convertToRoutrNumber(number, accessKeyId, appPeerAor = common_1.ROUTR_DEFAULT_PEER_AOR) {
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Where app-linked numbers point (add-media-node 4.1). Asterisk is retired (cleanbreak CB-B3):
3
- * every app-linked number goes to the media-node peer that Go media nodes register under with
2
+ * Where app-linked numbers point (add-roomr 4.1). Asterisk is retired (cleanbreak CB-B3):
3
+ * every app-linked number goes to the roomr peer that Go Roomr nodes register under with
4
4
  * least-sessions. `rebuild` is the only engine left; the type stays so repoint reports keep
5
5
  * their shape.
6
6
  */
7
7
  export type VoiceEngine = "rebuild";
8
8
  export type EngineRoutingConfig = {
9
- /** The media-node peer AOR (default sip:medianode@default). */
10
- mediaNodeAor?: string;
9
+ /** The roomr peer AOR (default sip:roomr@default). */
10
+ roomrNodeAor?: string;
11
11
  };
12
- export declare const MEDIA_NODE_PEER_AOR = "sip:medianode@default";
12
+ export declare const ROOMR_PEER_AOR = "sip:roomr@default";
13
13
  /** The AOR of the retired Asterisk peer, for moving numbers created before the rebuild. */
14
14
  export declare const LEGACY_ASTERISK_PEER_AOR = "sip:voice@default";
15
15
  export type NumberRouting = {
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.LEGACY_ASTERISK_PEER_AOR = exports.MEDIA_NODE_PEER_AOR = void 0;
12
+ exports.LEGACY_ASTERISK_PEER_AOR = exports.ROOMR_PEER_AOR = void 0;
13
13
  exports.createNumberRouting = createNumberRouting;
14
14
  /**
15
15
  * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
@@ -30,12 +30,12 @@ exports.createNumberRouting = createNumberRouting;
30
30
  * limitations under the License.
31
31
  */
32
32
  const common_1 = require("@fonoster/common");
33
- exports.MEDIA_NODE_PEER_AOR = common_1.ROUTR_DEFAULT_PEER_AOR;
33
+ exports.ROOMR_PEER_AOR = common_1.ROUTR_DEFAULT_PEER_AOR;
34
34
  /** The AOR of the retired Asterisk peer, for moving numbers created before the rebuild. */
35
35
  exports.LEGACY_ASTERISK_PEER_AOR = "sip:voice@default";
36
36
  function createNumberRouting(config = {}) {
37
37
  var _a;
38
- const aor = (_a = config.mediaNodeAor) !== null && _a !== void 0 ? _a : exports.MEDIA_NODE_PEER_AOR;
38
+ const aor = (_a = config.roomrNodeAor) !== null && _a !== void 0 ? _a : exports.ROOMR_PEER_AOR;
39
39
  return {
40
40
  engineFor: () => __awaiter(this, void 0, void 0, function* () { return "rebuild"; }),
41
41
  aorFor: () => __awaiter(this, void 0, void 0, function* () { return aor; }),
@@ -37,7 +37,7 @@ export type RepointChange = {
37
37
  to: string;
38
38
  };
39
39
  /**
40
- * Moves app-linked Numbers to the peer routing says (the media nodes). Routr stores `aorLink` on
40
+ * Moves app-linked Numbers to the peer routing says (the Roomr nodes). Routr stores `aorLink` on
41
41
  * each Number, so numbers created for Asterisk keep pointing at it until they are rewritten;
42
42
  * agent-linked Numbers are never touched. Returns what changed (or would,
43
43
  * with `dryRun`).
@@ -30,7 +30,7 @@ exports.repointNumbers = repointNumbers;
30
30
  */
31
31
  const common_1 = require("@fonoster/common");
32
32
  /**
33
- * Moves app-linked Numbers to the peer routing says (the media nodes). Routr stores `aorLink` on
33
+ * Moves app-linked Numbers to the peer routing says (the Roomr nodes). Routr stores `aorLink` on
34
34
  * each Number, so numbers created for Asterisk keep pointing at it until they are rewritten;
35
35
  * agent-linked Numbers are never touched. Returns what changed (or would,
36
36
  * with `dryRun`).
@@ -69,11 +69,11 @@ const engineRouting_1 = require("./engineRouting");
69
69
  const repointNumbers_1 = require("./repointNumbers");
70
70
  /**
71
71
  * Moves app-linked Numbers created before the rebuild (pointing at Asterisk's sip:voice@default)
72
- * to the media-node peer:
72
+ * to the roomr peer:
73
73
  *
74
74
  * node dist/numbers/repointNumbersCli.js <routing.json> [--apply] [--app <ref>]...
75
75
  *
76
- * routing.json holds an EngineRoutingConfig ({ mediaNodeAor }); `{}` uses sip:medianode@default.
76
+ * routing.json holds an EngineRoutingConfig ({ roomrNodeAor }); `{}` uses sip:roomr@default.
77
77
  * Without --apply it only prints what would change. ROUTR_API_ENDPOINT (default routr:51907)
78
78
  * points at Routr's API.
79
79
  */
@@ -45,12 +45,13 @@ export declare function filterByAccessKeyId<T extends {
45
45
  };
46
46
  }>(items: T[], accessKeyId: string): T[];
47
47
  /**
48
- * Handles pagination with filtering, continuing to fetch pages until:
49
- * - We have enough items (pageSize)
50
- * - We run out of pages
51
- * - We hit the max iterations limit
48
+ * Pages through Routr's items keeping only the caller's, so that following `nextPageToken` returns
49
+ * every kept item exactly once (management-api, "Lists page through every item once").
52
50
  *
53
- * This handles the case where the first pages contain items from other customers.
51
+ * Routr's page token is a cursor on the last ref it returned, so a page can end on any item: when
52
+ * the page fills in the middle of a backend batch, the token is the ref of the last item kept and
53
+ * the next call resumes right after it. Batches with none of the caller's items are skipped, up to
54
+ * `maxIterations` of them; the token then points past the last batch read.
54
55
  *
55
56
  * @template TInput - The type of items returned from the API
56
57
  * @template TOutput - The type of items after filtering/transformation (defaults to TInput)
@@ -33,71 +33,50 @@ function filterByAccessKeyId(items, accessKeyId) {
33
33
  return items.filter((item) => { var _a; return ((_a = item.extended) === null || _a === void 0 ? void 0 : _a.accessKeyId) === accessKeyId; });
34
34
  }
35
35
  /**
36
- * Handles pagination with filtering, continuing to fetch pages until:
37
- * - We have enough items (pageSize)
38
- * - We run out of pages
39
- * - We hit the max iterations limit
36
+ * Pages through Routr's items keeping only the caller's, so that following `nextPageToken` returns
37
+ * every kept item exactly once (management-api, "Lists page through every item once").
40
38
  *
41
- * This handles the case where the first pages contain items from other customers.
39
+ * Routr's page token is a cursor on the last ref it returned, so a page can end on any item: when
40
+ * the page fills in the middle of a backend batch, the token is the ref of the last item kept and
41
+ * the next call resumes right after it. Batches with none of the caller's items are skipped, up to
42
+ * `maxIterations` of them; the token then points past the last batch read.
42
43
  *
43
44
  * @template TInput - The type of items returned from the API
44
45
  * @template TOutput - The type of items after filtering/transformation (defaults to TInput)
45
46
  */
46
47
  function paginateWithFiltering(options) {
47
48
  return __awaiter(this, void 0, void 0, function* () {
49
+ var _a, _b;
48
50
  const { pageSize, pageToken, fetchPage, filterItems, maxIterations = 10 } = options;
49
- const normalizedPageToken = normalizePageToken(pageToken);
50
51
  const items = [];
51
- let nextPageToken = "";
52
- let currentPageToken = normalizedPageToken;
53
- let hasMorePages = true;
54
- let iterations = 0;
55
- while (items.length < pageSize &&
56
- hasMorePages &&
57
- iterations < maxIterations) {
58
- iterations++;
59
- // Request more items to account for filtering
60
- const backendRequestedSize = pageSize * 2;
61
- const response = yield fetchPage(currentPageToken, backendRequestedSize);
62
- if (!response || !response.items) {
63
- hasMorePages = false;
64
- nextPageToken = "";
65
- break;
66
- }
67
- const filteredItems = filterItems(response.items);
68
- items.push(...filteredItems);
69
- const backendNextPageToken = normalizeNextPageToken(response.nextPageToken);
70
- // If we got no items from backend or no nextPageToken, we're done
71
- if (response.items.length === 0 || !backendNextPageToken) {
72
- hasMorePages = false;
73
- nextPageToken = "";
74
- break;
75
- }
76
- // Check if backend returned fewer items than requested (indicating last page)
77
- // If backend returned exactly what we asked for or more, there might be more items
78
- const backendHasMoreItems = response.items.length >= backendRequestedSize;
79
- // If we've filled the page, we're done
80
- if (items.length >= pageSize) {
81
- // We filled the page, so there might be more items
82
- // Only return nextPageToken if backend indicates there are more items
83
- nextPageToken = backendHasMoreItems ? backendNextPageToken : "";
84
- break;
85
- }
86
- // If we got filtered items in this iteration, return them
87
- // Only continue if we got 0 filtered items (meaning all items were from other customers)
88
- if (filteredItems.length === 0) {
89
- // No items matched in this page, continue to next page
90
- currentPageToken = backendNextPageToken;
91
- nextPageToken = backendNextPageToken;
92
- }
93
- else {
94
- nextPageToken = backendHasMoreItems ? backendNextPageToken : "";
95
- break;
52
+ let cursor = normalizePageToken(pageToken);
53
+ for (let i = 0; i < maxIterations; i++) {
54
+ const requested = pageSize * 2;
55
+ const response = yield fetchPage(cursor, requested);
56
+ const batch = (_a = response === null || response === void 0 ? void 0 : response.items) !== null && _a !== void 0 ? _a : [];
57
+ const next = normalizeNextPageToken(response === null || response === void 0 ? void 0 : response.nextPageToken);
58
+ // Routr may answer a token on its last page too: a short batch that had some of the caller's
59
+ // items is the end; a short one with none of them is followed, as before.
60
+ const short = batch.length < requested;
61
+ let kept = 0;
62
+ for (const [index, item] of batch.entries()) {
63
+ const mine = filterItems([item]);
64
+ if (mine.length === 0)
65
+ continue;
66
+ kept += mine.length;
67
+ items.push(...mine);
68
+ if (items.length >= pageSize) {
69
+ const lastOfAll = (!next || short) && index === batch.length - 1;
70
+ return {
71
+ items,
72
+ nextPageToken: lastOfAll ? "" : ((_b = item.ref) !== null && _b !== void 0 ? _b : "")
73
+ };
74
+ }
96
75
  }
76
+ if (batch.length === 0 || !next || (short && kept > 0))
77
+ return { items, nextPageToken: "" };
78
+ cursor = next;
97
79
  }
98
- return {
99
- items: items.slice(0, pageSize),
100
- nextPageToken
101
- };
80
+ return { items, nextPageToken: cursor !== null && cursor !== void 0 ? cursor : "" };
102
81
  });
103
82
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import { GrpcErrorMessage } from "@fonoster/common";
20
+ /**
21
+ * Whether a SIP name (a trunk's inbound URI, a domain) is one label under `root`
22
+ * (e.g. `acme.sip.example.com` under `sip.example.com`). Without a root
23
+ * (features.sip.names: any) every name passes; the request schema still checks
24
+ * it's a host or an IP.
25
+ */
26
+ export declare function isSipNameAllowed(name: string, root?: string): boolean;
27
+ type Handler<R> = (call: {
28
+ request: R;
29
+ }, callback: (error?: GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
30
+ /**
31
+ * Rejects a create or update whose `field` isn't one label under `root` with
32
+ * INVALID_ARGUMENT. An update that leaves the field blank keeps the current value.
33
+ */
34
+ export declare function withSipNameCheck<R>(handler: Handler<R>, field: string, what: string, root?: string): Handler<R>;
35
+ export {};
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.isSipNameAllowed = isSipNameAllowed;
13
+ exports.withSipNameCheck = withSipNameCheck;
14
+ const grpc_js_1 = require("@grpc/grpc-js");
15
+ /** One DNS label: lowercase letters, digits and inner dashes. */
16
+ const LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
17
+ /**
18
+ * Whether a SIP name (a trunk's inbound URI, a domain) is one label under `root`
19
+ * (e.g. `acme.sip.example.com` under `sip.example.com`). Without a root
20
+ * (features.sip.names: any) every name passes; the request schema still checks
21
+ * it's a host or an IP.
22
+ */
23
+ function isSipNameAllowed(name, root) {
24
+ if (!root)
25
+ return true;
26
+ const suffix = `.${root}`;
27
+ return name.endsWith(suffix) && LABEL.test(name.slice(0, -suffix.length));
28
+ }
29
+ /**
30
+ * Rejects a create or update whose `field` isn't one label under `root` with
31
+ * INVALID_ARGUMENT. An update that leaves the field blank keeps the current value.
32
+ */
33
+ function withSipNameCheck(handler, field, what, root) {
34
+ return (call, callback) => __awaiter(this, void 0, void 0, function* () {
35
+ const value = call.request[field];
36
+ const name = typeof value === "string" ? value : undefined;
37
+ if (name && !isSipNameAllowed(name, root)) {
38
+ callback({
39
+ code: grpc_js_1.status.INVALID_ARGUMENT,
40
+ message: `The ${what} must be one label under ${root}, such as acme.${root} at "${field}"`
41
+ });
42
+ return;
43
+ }
44
+ return handler(call, callback);
45
+ });
46
+ }
@@ -1,5 +1,11 @@
1
1
  import { ClientOptions } from "../types";
2
- declare function buildService(clientOptions: ClientOptions): {
2
+ /**
3
+ * `namesRoot` (e.g. `sip.example.com`) makes every inbound URI one label under
4
+ * it; leave it out to accept any host or IP.
5
+ */
6
+ declare function buildService(clientOptions: ClientOptions, options?: {
7
+ namesRoot?: string;
8
+ }): {
3
9
  definition: {
4
10
  serviceName: string;
5
11
  pckg: string;
@@ -24,8 +24,13 @@ exports.buildService = buildService;
24
24
  */
25
25
  const sdk_1 = __importDefault(require("@routr/sdk"));
26
26
  const checkRefs_1 = require("../resources/checkRefs");
27
+ const sipNames_1 = require("../resources/sipNames");
27
28
  const operations_1 = require("./operations");
28
- function buildService(clientOptions) {
29
+ /**
30
+ * `namesRoot` (e.g. `sip.example.com`) makes every inbound URI one label under
31
+ * it; leave it out to accept any host or IP.
32
+ */
33
+ function buildService(clientOptions, options = {}) {
29
34
  const client = new sdk_1.default.Trunks(clientOptions);
30
35
  const checkRefs = (0, checkRefs_1.createCheckRefs)((0, checkRefs_1.createRefLookups)(clientOptions));
31
36
  return {
@@ -36,8 +41,8 @@ function buildService(clientOptions) {
36
41
  proto: "trunks.proto"
37
42
  },
38
43
  handlers: {
39
- createTrunk: (0, operations_1.createTrunk)(client, checkRefs),
40
- updateTrunk: (0, operations_1.updateTrunk)(client, checkRefs),
44
+ createTrunk: (0, sipNames_1.withSipNameCheck)((0, operations_1.createTrunk)(client, checkRefs), "inboundUri", "inbound URI", options.namesRoot),
45
+ updateTrunk: (0, sipNames_1.withSipNameCheck)((0, operations_1.updateTrunk)(client, checkRefs), "inboundUri", "inbound URI", options.namesRoot),
41
46
  getTrunk: (0, operations_1.getTrunk)(client),
42
47
  listTrunks: (0, operations_1.listTrunks)(client),
43
48
  deleteTrunk: (0, operations_1.deleteTrunk)(client)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonoster/sipnet",
3
- "version": "0.30.0",
3
+ "version": "0.31.1",
4
4
  "description": "Routr-based SIP stack",
5
5
  "author": "Pedro Sanders <psanders@fonoster.com>",
6
6
  "homepage": "https://github.com/fonoster/fonoster#readme",
@@ -13,19 +13,18 @@
13
13
  },
14
14
  "scripts": {
15
15
  "prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
16
- "build": "tsc -b tsconfig.json",
17
- "clean": "rimraf ./dist node_modules tsconfig.tsbuildinfo"
16
+ "build": "tsc -b tsconfig.json"
18
17
  },
19
18
  "bin": {
20
19
  "fonoster": "./dist/index.js"
21
20
  },
22
21
  "dependencies": {
23
- "@fonoster/common": "^0.30.0",
24
- "@fonoster/identity": "^0.30.0",
25
- "@fonoster/logger": "^0.30.0",
26
- "@fonoster/types": "^0.30.0",
22
+ "@fonoster/common": "^0.31.1",
23
+ "@fonoster/identity": "^0.31.1",
24
+ "@fonoster/logger": "^0.31.0",
25
+ "@fonoster/types": "^0.31.0",
27
26
  "@grpc/grpc-js": "~1.14.6",
28
- "@routr/sdk": "2.15.2",
27
+ "@routr/sdk": "2.19.0",
29
28
  "zod": "^3.23.8"
30
29
  },
31
30
  "files": [
@@ -41,5 +40,5 @@
41
40
  "bugs": {
42
41
  "url": "https://github.com/fonoster/fonoster/issues"
43
42
  },
44
- "gitHead": "b59b06a3ef325abe3f188c0fb87cc22d50ed5471"
43
+ "gitHead": "c88962ba4c111de08d156d5b86770add1cade544"
45
44
  }