@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.
- package/dist/acls/updateAcl.d.ts +0 -19
- package/dist/acls/updateAcl.js +5 -4
- package/dist/agents/buildService.js +4 -2
- package/dist/agents/operations.d.ts +3 -2
- package/dist/agents/operations.js +5 -4
- package/dist/domains/buildService.d.ts +7 -1
- package/dist/domains/buildService.js +10 -3
- package/dist/domains/operations.d.ts +3 -2
- package/dist/domains/operations.js +5 -4
- package/dist/numbers/buildService.d.ts +3 -1
- package/dist/numbers/buildService.js +6 -3
- package/dist/numbers/convertToRoutrNumber.d.ts +6 -2
- package/dist/numbers/convertToRoutrNumber.js +8 -4
- package/dist/numbers/createNumber.d.ts +3 -1
- package/dist/numbers/createNumber.js +13 -4
- package/dist/numbers/engineRouting.d.ts +29 -0
- package/dist/numbers/engineRouting.js +44 -0
- package/dist/numbers/getNumber.d.ts +0 -19
- package/dist/numbers/getNumber.js +6 -6
- package/dist/numbers/index.d.ts +2 -0
- package/dist/numbers/index.js +16 -0
- package/dist/numbers/repointNumbers.d.ts +49 -0
- package/dist/numbers/repointNumbers.js +83 -0
- package/dist/numbers/repointNumbersCli.d.ts +1 -0
- package/dist/numbers/repointNumbersCli.js +106 -0
- package/dist/numbers/updateNumber.d.ts +4 -2
- package/dist/numbers/updateNumber.js +20 -7
- package/dist/resources/checkRefs.d.ts +26 -0
- package/dist/resources/checkRefs.js +138 -0
- package/dist/resources/createResource.d.ts +2 -1
- package/dist/resources/createResource.js +5 -2
- package/dist/resources/paginationUtils.d.ts +6 -5
- package/dist/resources/paginationUtils.js +34 -55
- package/dist/resources/sipNames.d.ts +35 -0
- package/dist/resources/sipNames.js +46 -0
- package/dist/resources/updateResource.d.ts +2 -1
- package/dist/resources/updateResource.js +7 -2
- package/dist/trunks/buildService.d.ts +7 -1
- package/dist/trunks/buildService.js +10 -3
- package/dist/trunks/operations.d.ts +10 -3
- package/dist/trunks/operations.js +40 -4
- 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 {
|
|
19
|
+
import { NumberPreconditionsCheck } from "@fonoster/common";
|
|
20
20
|
import { NumbersApi } from "@fonoster/types";
|
|
21
|
-
|
|
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
|
|
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
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
49
|
-
* -
|
|
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
|
-
*
|
|
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
|
-
*
|
|
37
|
-
* -
|
|
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
|
-
*
|
|
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
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|