@fonoster/sipnet 0.27.0 → 0.31.0

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.
Files changed (42) hide show
  1. package/dist/acls/updateAcl.d.ts +0 -19
  2. package/dist/acls/updateAcl.js +5 -4
  3. package/dist/agents/buildService.js +4 -2
  4. package/dist/agents/operations.d.ts +3 -2
  5. package/dist/agents/operations.js +5 -4
  6. package/dist/domains/buildService.d.ts +7 -1
  7. package/dist/domains/buildService.js +10 -3
  8. package/dist/domains/operations.d.ts +3 -2
  9. package/dist/domains/operations.js +5 -4
  10. package/dist/numbers/buildService.d.ts +3 -1
  11. package/dist/numbers/buildService.js +6 -3
  12. package/dist/numbers/convertToRoutrNumber.d.ts +6 -2
  13. package/dist/numbers/convertToRoutrNumber.js +8 -4
  14. package/dist/numbers/createNumber.d.ts +3 -1
  15. package/dist/numbers/createNumber.js +13 -4
  16. package/dist/numbers/engineRouting.d.ts +29 -0
  17. package/dist/numbers/engineRouting.js +44 -0
  18. package/dist/numbers/getNumber.d.ts +0 -19
  19. package/dist/numbers/getNumber.js +6 -6
  20. package/dist/numbers/index.d.ts +2 -0
  21. package/dist/numbers/index.js +16 -0
  22. package/dist/numbers/repointNumbers.d.ts +49 -0
  23. package/dist/numbers/repointNumbers.js +83 -0
  24. package/dist/numbers/repointNumbersCli.d.ts +1 -0
  25. package/dist/numbers/repointNumbersCli.js +106 -0
  26. package/dist/numbers/updateNumber.d.ts +4 -2
  27. package/dist/numbers/updateNumber.js +20 -7
  28. package/dist/resources/checkRefs.d.ts +26 -0
  29. package/dist/resources/checkRefs.js +138 -0
  30. package/dist/resources/createResource.d.ts +2 -1
  31. package/dist/resources/createResource.js +5 -2
  32. package/dist/resources/paginationUtils.d.ts +6 -5
  33. package/dist/resources/paginationUtils.js +34 -55
  34. package/dist/resources/sipNames.d.ts +35 -0
  35. package/dist/resources/sipNames.js +46 -0
  36. package/dist/resources/updateResource.d.ts +2 -1
  37. package/dist/resources/updateResource.js +7 -2
  38. package/dist/trunks/buildService.d.ts +7 -1
  39. package/dist/trunks/buildService.js +10 -3
  40. package/dist/trunks/operations.d.ts +10 -3
  41. package/dist/trunks/operations.js +40 -4
  42. package/package.json +9 -10
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ /**
49
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
50
+ * http://github.com/fonoster/fonoster
51
+ *
52
+ * This file is part of Fonoster
53
+ *
54
+ * Licensed under the MIT License (the "License");
55
+ * you may not use this file except in compliance with
56
+ * the License. You may obtain a copy of the License at
57
+ *
58
+ * https://opensource.org/licenses/MIT
59
+ *
60
+ * Unless required by applicable law or agreed to in writing, software
61
+ * distributed under the License is distributed on an "AS IS" BASIS,
62
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
63
+ * See the License for the specific language governing permissions and
64
+ * limitations under the License.
65
+ */
66
+ const fs = __importStar(require("fs"));
67
+ const sdk_1 = __importDefault(require("@routr/sdk"));
68
+ const engineRouting_1 = require("./engineRouting");
69
+ const repointNumbers_1 = require("./repointNumbers");
70
+ /**
71
+ * Moves app-linked Numbers created before the rebuild (pointing at Asterisk's sip:voice@default)
72
+ * to the roomr peer:
73
+ *
74
+ * node dist/numbers/repointNumbersCli.js <routing.json> [--apply] [--app <ref>]...
75
+ *
76
+ * routing.json holds an EngineRoutingConfig ({ roomrNodeAor }); `{}` uses sip:roomr@default.
77
+ * Without --apply it only prints what would change. ROUTR_API_ENDPOINT (default routr:51907)
78
+ * points at Routr's API.
79
+ */
80
+ function main(argv) {
81
+ return __awaiter(this, void 0, void 0, function* () {
82
+ var _a;
83
+ const file = argv[0];
84
+ if (!file) {
85
+ console.error("usage: repointNumbersCli <routing.json> [--apply] [--app <ref>]...");
86
+ process.exit(2);
87
+ }
88
+ const config = JSON.parse(fs.readFileSync(file, "utf8"));
89
+ const appRefs = argv
90
+ .map((a, i) => (a === "--app" ? argv[i + 1] : undefined))
91
+ .filter((a) => !!a);
92
+ const client = new sdk_1.default.Numbers({
93
+ endpoint: (_a = process.env.ROUTR_API_ENDPOINT) !== null && _a !== void 0 ? _a : "routr:51907",
94
+ insecure: true
95
+ });
96
+ const changes = yield (0, repointNumbers_1.repointNumbers)(client, (0, engineRouting_1.createNumberRouting)(config), {
97
+ dryRun: !argv.includes("--apply"),
98
+ appRefs: appRefs.length ? appRefs : undefined
99
+ });
100
+ console.log(JSON.stringify({ applied: argv.includes("--apply"), changes }, null, 2));
101
+ });
102
+ }
103
+ main(process.argv.slice(2)).catch((e) => {
104
+ console.error(e.message);
105
+ process.exit(1);
106
+ });
@@ -16,9 +16,11 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
- import { GrpcErrorMessage, NumberPreconditionsCheck } from "@fonoster/common";
19
+ import { NumberPreconditionsCheck } from "@fonoster/common";
20
20
  import { NumbersApi } from "@fonoster/types";
21
- declare function updateNumber(api: NumbersApi, checkNumberPreconditions: NumberPreconditionsCheck): (call: {
21
+ import { CheckRefs } from "../resources/checkRefs";
22
+ import { NumberRouting } from "./engineRouting";
23
+ declare function updateNumber(api: NumbersApi, checkNumberPreconditions: NumberPreconditionsCheck, routing?: NumberRouting, checkRefs?: CheckRefs): (call: {
22
24
  request: unknown;
23
25
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
24
26
  export { updateNumber };
@@ -29,17 +29,30 @@ exports.updateNumber = updateNumber;
29
29
  * limitations under the License.
30
30
  */
31
31
  const common_1 = require("@fonoster/common");
32
+ const identity_1 = require("@fonoster/identity");
32
33
  const logger_1 = require("@fonoster/logger");
34
+ const checkRefs_1 = require("../resources/checkRefs");
33
35
  const convertToRoutrNumber_1 = require("./convertToRoutrNumber");
34
36
  const logger = (0, logger_1.getLogger)({ service: "sipnet", filePath: __filename });
35
- function updateNumber(api, checkNumberPreconditions) {
36
- const fn = (call, callback) => __awaiter(this, void 0, void 0, function* () {
37
+ function updateNumber(api, checkNumberPreconditions, routing, checkRefs) {
38
+ const fn = (call) => __awaiter(this, void 0, void 0, function* () {
39
+ var _a;
37
40
  const { request } = call;
38
- // Validates that the appRef or agentAor exists in the system
39
- yield checkNumberPreconditions(request);
41
+ // Validates that the appRef exists in the caller's workspace (RV-B4)
42
+ const accessKeyId = (0, common_1.getAccessKeyIdFromCall)(call);
43
+ yield checkNumberPreconditions(Object.assign(Object.assign({}, request), { accessKeyId }));
44
+ // ...and the trunk too (H7)
45
+ yield (checkRefs === null || checkRefs === void 0 ? void 0 : checkRefs(accessKeyId, (0, checkRefs_1.numberRefs)(request)));
40
46
  logger.verbose("call to updateNumber", Object.assign({}, request));
41
- const response = yield api.updateNumber((0, convertToRoutrNumber_1.convertToRoutrNumberUpdate)(request));
42
- callback(null, response);
47
+ // The number's telUrl matters only for per-number pins, so it is read only with routing.
48
+ const appPeerAor = routing && request.appRef
49
+ ? yield routing.aorFor({
50
+ appRef: request.appRef,
51
+ telUrl: (_a = (yield api.getNumber(request.ref))) === null || _a === void 0 ? void 0 : _a.telUrl
52
+ })
53
+ : undefined;
54
+ return yield api.updateNumber((0, convertToRoutrNumber_1.convertToRoutrNumberUpdate)(request, appPeerAor));
43
55
  });
44
- return (0, common_1.withErrorHandlingAndValidation)(fn, common_1.Validators.updateNumberRequestSchema);
56
+ // Only the caller's own numbers can be updated, like every other resource.
57
+ return (0, common_1.withErrorHandlingAndValidation)((0, identity_1.withAccess)(fn, ((ref) => api.getNumber(ref))), common_1.Validators.updateNumberRequestSchema);
45
58
  }
@@ -0,0 +1,26 @@
1
+ import { ClientOptions } from "../types";
2
+ type RefKind = "Acl" | "Credentials" | "Domain" | "Number" | "Trunk";
3
+ type OwnedResource = {
4
+ extended?: Record<string, unknown> | null;
5
+ } | null | undefined;
6
+ type RefLookups = Record<RefKind, (ref: string) => Promise<OwnedResource>>;
7
+ /** One reference in a request: the request field, what it names and its value. */
8
+ type RefField = {
9
+ field: string;
10
+ kind: RefKind;
11
+ ref?: string | null;
12
+ };
13
+ type CheckRefs = (accessKeyId: string | undefined, refs: RefField[]) => Promise<void>;
14
+ /** A request's references, checked before it reaches Routr. */
15
+ type CheckRequestRefs<R> = (request: R, accessKeyId: string | undefined) => Promise<void>;
16
+ declare function createCheckRefs(lookups: Partial<RefLookups>): CheckRefs;
17
+ /** The production lookups: Routr's API, one client per kind. */
18
+ declare function createRefLookups(clientOptions: ClientOptions): RefLookups;
19
+ type Refs = Record<string, unknown>;
20
+ declare const agentRefs: (r: Refs) => RefField[];
21
+ declare const trunkRefs: (r: Refs) => RefField[];
22
+ declare const domainRefs: (r: Refs) => RefField[];
23
+ declare const numberRefs: (r: Refs) => RefField[];
24
+ /** The reference check for one resource type's requests. */
25
+ declare function refsChecker<R>(checkRefs: CheckRefs | undefined, refsOf: (request: Refs) => RefField[]): CheckRequestRefs<R> | undefined;
26
+ export { agentRefs, CheckRefs, CheckRequestRefs, createCheckRefs, createRefLookups, domainRefs, numberRefs, RefField, RefKind, RefLookups, refsChecker, trunkRefs };
@@ -0,0 +1,138 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.trunkRefs = exports.numberRefs = exports.domainRefs = exports.agentRefs = void 0;
16
+ exports.createCheckRefs = createCheckRefs;
17
+ exports.createRefLookups = createRefLookups;
18
+ exports.refsChecker = refsChecker;
19
+ /**
20
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
21
+ * http://github.com/fonoster/fonoster
22
+ *
23
+ * This file is part of Fonoster
24
+ *
25
+ * Licensed under the MIT License (the "License");
26
+ * you may not use this file except in compliance with
27
+ * the License. You may obtain a copy of the License at
28
+ *
29
+ * https://opensource.org/licenses/MIT
30
+ *
31
+ * Unless required by applicable law or agreed to in writing, software
32
+ * distributed under the License is distributed on an "AS IS" BASIS,
33
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34
+ * See the License for the specific language governing permissions and
35
+ * limitations under the License.
36
+ */
37
+ const common_1 = require("@fonoster/common");
38
+ const grpc_js_1 = require("@grpc/grpc-js");
39
+ const sdk_1 = __importDefault(require("@routr/sdk"));
40
+ const isNotFound = (e) => (e === null || e === void 0 ? void 0 : e.code) === grpc_js_1.status.NOT_FOUND;
41
+ function createCheckRefs(lookups) {
42
+ return (accessKeyId, refs) => __awaiter(this, void 0, void 0, function* () {
43
+ var _a;
44
+ const given = refs.filter((r) => typeof r.ref === "string" && r.ref !== "");
45
+ if (given.length === 0)
46
+ return;
47
+ if (!accessKeyId) {
48
+ throw new common_1.GrpcError(grpc_js_1.status.PERMISSION_DENIED, "the caller's workspace is required to reference other resources");
49
+ }
50
+ for (const { field, kind, ref } of given) {
51
+ const lookup = lookups[kind];
52
+ if (!lookup)
53
+ throw new Error(`no lookup for ${kind} references`);
54
+ let resource;
55
+ try {
56
+ resource = yield lookup(ref);
57
+ }
58
+ catch (e) {
59
+ if (!isNotFound(e))
60
+ throw e;
61
+ resource = undefined;
62
+ }
63
+ // Another workspace's resource is reported like a missing one, so refs of other
64
+ // workspaces are not confirmed.
65
+ if (((_a = resource === null || resource === void 0 ? void 0 : resource.extended) === null || _a === void 0 ? void 0 : _a.accessKeyId) !== accessKeyId) {
66
+ throw new common_1.GrpcError(grpc_js_1.status.INVALID_ARGUMENT, `${field} ${ref} not found`);
67
+ }
68
+ }
69
+ });
70
+ }
71
+ /** The production lookups: Routr's API, one client per kind. */
72
+ function createRefLookups(clientOptions) {
73
+ const acls = new sdk_1.default.Acls(clientOptions);
74
+ const credentials = new sdk_1.default.Credentials(clientOptions);
75
+ const domains = new sdk_1.default.Domains(clientOptions);
76
+ const numbers = new sdk_1.default.Numbers(clientOptions);
77
+ const trunks = new sdk_1.default.Trunks(clientOptions);
78
+ return {
79
+ Acl: (ref) => acls.getAcl(ref),
80
+ Credentials: (ref) => credentials.getCredentials(ref),
81
+ Domain: (ref) => domains.getDomain(ref),
82
+ Number: (ref) => numbers.getNumber(ref),
83
+ Trunk: (ref) => trunks.getTrunk(ref)
84
+ };
85
+ }
86
+ const agentRefs = (r) => [
87
+ { field: "domainRef", kind: "Domain", ref: r.domainRef },
88
+ {
89
+ field: "credentialsRef",
90
+ kind: "Credentials",
91
+ ref: r.credentialsRef
92
+ }
93
+ ];
94
+ exports.agentRefs = agentRefs;
95
+ const trunkRefs = (r) => [
96
+ {
97
+ field: "accessControlListRef",
98
+ kind: "Acl",
99
+ ref: r.accessControlListRef
100
+ },
101
+ {
102
+ field: "inboundCredentialsRef",
103
+ kind: "Credentials",
104
+ ref: r.inboundCredentialsRef
105
+ },
106
+ {
107
+ field: "outboundCredentialsRef",
108
+ kind: "Credentials",
109
+ ref: r.outboundCredentialsRef
110
+ }
111
+ ];
112
+ exports.trunkRefs = trunkRefs;
113
+ const domainRefs = (r) => {
114
+ var _a;
115
+ return [
116
+ {
117
+ field: "accessControlListRef",
118
+ kind: "Acl",
119
+ ref: r.accessControlListRef
120
+ },
121
+ ...((_a = r.egressPolicies) !== null && _a !== void 0 ? _a : []).map((p, i) => ({
122
+ field: `egressPolicies[${i}].numberRef`,
123
+ kind: "Number",
124
+ ref: p === null || p === void 0 ? void 0 : p.numberRef
125
+ }))
126
+ ];
127
+ };
128
+ exports.domainRefs = domainRefs;
129
+ const numberRefs = (r) => [
130
+ { field: "trunkRef", kind: "Trunk", ref: r.trunkRef }
131
+ ];
132
+ exports.numberRefs = numberRefs;
133
+ /** The reference check for one resource type's requests. */
134
+ function refsChecker(checkRefs, refsOf) {
135
+ return checkRefs
136
+ ? (request, accessKeyId) => checkRefs(accessKeyId, refsOf(request))
137
+ : undefined;
138
+ }
@@ -18,7 +18,8 @@
18
18
  */
19
19
  import { GrpcErrorMessage } from "@fonoster/common";
20
20
  import { z } from "zod";
21
- declare function createResource<T, R, U>(api: U, resource: string, schema: z.ZodSchema): (call: {
21
+ import { CheckRequestRefs } from "./checkRefs";
22
+ declare function createResource<T, R, U>(api: U, resource: string, schema: z.ZodSchema, checkRefs?: CheckRequestRefs<R>): (call: {
22
23
  request: unknown;
23
24
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
24
25
  export { createResource };
@@ -31,11 +31,14 @@ exports.createResource = createResource;
31
31
  const common_1 = require("@fonoster/common");
32
32
  const logger_1 = require("@fonoster/logger");
33
33
  const logger = (0, logger_1.getLogger)({ service: "sipnet", filePath: __filename });
34
- function createResource(api, resource, schema) {
34
+ function createResource(api, resource, schema, checkRefs) {
35
35
  const fn = (call, callback) => __awaiter(this, void 0, void 0, function* () {
36
36
  const { request } = call;
37
37
  const accessKeyId = (0, common_1.getAccessKeyIdFromCall)(call);
38
- logger.verbose(`call to create${resource}`, Object.assign(Object.assign({}, request), { accessKeyId }));
38
+ // Never the request itself: credentials and trunks carry passwords (M11).
39
+ logger.verbose(`call to create${resource}`, { accessKeyId });
40
+ // Referenced credentials, ACLs, domains, numbers and trunks must be the caller's (H7).
41
+ yield (checkRefs === null || checkRefs === void 0 ? void 0 : checkRefs(request, accessKeyId));
39
42
  const response = yield api[`create${resource}`](Object.assign(Object.assign({}, request), { extended: {
40
43
  accessKeyId
41
44
  } }));
@@ -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,6 @@
1
1
  import { z } from "zod";
2
- declare function updateResource<T, R, U>(api: U, resource: string, schema: z.ZodSchema): (call: {
2
+ import { CheckRequestRefs } from "./checkRefs";
3
+ declare function updateResource<T, R, U>(api: U, resource: string, schema: z.ZodSchema, checkRefs?: CheckRequestRefs<R>): (call: {
3
4
  request: unknown;
4
5
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
5
6
  export { updateResource };
@@ -32,10 +32,15 @@ const common_1 = require("@fonoster/common");
32
32
  const identity_1 = require("@fonoster/identity");
33
33
  const logger_1 = require("@fonoster/logger");
34
34
  const logger = (0, logger_1.getLogger)({ service: "sipnet", filePath: __filename });
35
- function updateResource(api, resource, schema) {
35
+ function updateResource(api, resource, schema, checkRefs) {
36
36
  const fn = (call) => __awaiter(this, void 0, void 0, function* () {
37
37
  const { request } = call;
38
- logger.verbose(`call to update${resource}`, Object.assign({}, request));
38
+ // Never the request itself: credentials and trunks carry passwords (M11).
39
+ logger.verbose(`call to update${resource}`, {
40
+ ref: request.ref
41
+ });
42
+ // Referenced credentials, ACLs, domains, numbers and trunks must be the caller's (H7).
43
+ yield (checkRefs === null || checkRefs === void 0 ? void 0 : checkRefs(request, (0, common_1.getAccessKeyIdFromCall)(call)));
39
44
  return yield api[`update${resource}`](request);
40
45
  });
41
46
  return (0, common_1.withErrorHandlingAndValidation)((0, identity_1.withAccess)(fn, (ref) => api[`get${resource}`](ref)), schema);
@@ -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;
@@ -23,9 +23,16 @@ exports.buildService = buildService;
23
23
  * limitations under the License.
24
24
  */
25
25
  const sdk_1 = __importDefault(require("@routr/sdk"));
26
+ const checkRefs_1 = require("../resources/checkRefs");
27
+ const sipNames_1 = require("../resources/sipNames");
26
28
  const operations_1 = require("./operations");
27
- 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 = {}) {
28
34
  const client = new sdk_1.default.Trunks(clientOptions);
35
+ const checkRefs = (0, checkRefs_1.createCheckRefs)((0, checkRefs_1.createRefLookups)(clientOptions));
29
36
  return {
30
37
  definition: {
31
38
  serviceName: "Trunks",
@@ -34,8 +41,8 @@ function buildService(clientOptions) {
34
41
  proto: "trunks.proto"
35
42
  },
36
43
  handlers: {
37
- createTrunk: (0, operations_1.createTrunk)(client),
38
- updateTrunk: (0, operations_1.updateTrunk)(client),
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),
39
46
  getTrunk: (0, operations_1.getTrunk)(client),
40
47
  listTrunks: (0, operations_1.listTrunks)(client),
41
48
  deleteTrunk: (0, operations_1.deleteTrunk)(client)