@fonoster/sipnet 0.36.0 → 0.38.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/numbers/buildService.d.ts +11 -2
- package/dist/numbers/buildService.js +13 -3
- package/dist/numbers/linkTwilio.d.ts +26 -0
- package/dist/numbers/linkTwilio.js +228 -0
- package/dist/numbers/twilioLinkErrorToGrpc.d.ts +30 -0
- package/dist/numbers/twilioLinkErrorToGrpc.js +65 -0
- package/package.json +7 -6
|
@@ -19,8 +19,14 @@
|
|
|
19
19
|
import { NumberPreconditionsCheck } from "@fonoster/common";
|
|
20
20
|
import { ClientOptions } from "../types";
|
|
21
21
|
import { NumberRouting } from "./engineRouting";
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* `routing` picks the peer app-linked numbers point at (default: the roomr peer). `twilioLink`
|
|
24
|
+
* is the install's PSTN root and public signaling addresses, for LinkTwilio.
|
|
25
|
+
*/
|
|
26
|
+
declare function buildService(clientOptions: ClientOptions, checkNumberPreconditions: NumberPreconditionsCheck, routing?: NumberRouting, twilioLink?: {
|
|
27
|
+
pstnRoot: string;
|
|
28
|
+
signalingAddresses: string[];
|
|
29
|
+
}): {
|
|
24
30
|
definition: {
|
|
25
31
|
serviceName: string;
|
|
26
32
|
pckg: string;
|
|
@@ -43,6 +49,9 @@ declare function buildService(clientOptions: ClientOptions, checkNumberPrecondit
|
|
|
43
49
|
deleteNumber: (call: {
|
|
44
50
|
request: unknown;
|
|
45
51
|
}, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
|
|
52
|
+
linkTwilio: (call: {
|
|
53
|
+
request: unknown;
|
|
54
|
+
}, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
|
|
46
55
|
};
|
|
47
56
|
};
|
|
48
57
|
export { buildService };
|
|
@@ -8,11 +8,18 @@ const sdk_1 = __importDefault(require("@routr/sdk"));
|
|
|
8
8
|
const checkRefs_1 = require("../resources/checkRefs");
|
|
9
9
|
const createNumber_1 = require("./createNumber");
|
|
10
10
|
const getNumber_1 = require("./getNumber");
|
|
11
|
+
const linkTwilio_1 = require("./linkTwilio");
|
|
11
12
|
const listNumbers_1 = require("./listNumbers");
|
|
12
13
|
const operations_1 = require("./operations");
|
|
13
14
|
const updateNumber_1 = require("./updateNumber");
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* `routing` picks the peer app-linked numbers point at (default: the roomr peer). `twilioLink`
|
|
17
|
+
* is the install's PSTN root and public signaling addresses, for LinkTwilio.
|
|
18
|
+
*/
|
|
19
|
+
function buildService(clientOptions, checkNumberPreconditions, routing, twilioLink = {
|
|
20
|
+
pstnRoot: "",
|
|
21
|
+
signalingAddresses: []
|
|
22
|
+
}) {
|
|
16
23
|
const client = new sdk_1.default.Numbers(clientOptions);
|
|
17
24
|
const checkRefs = (0, checkRefs_1.createCheckRefs)((0, checkRefs_1.createRefLookups)(clientOptions));
|
|
18
25
|
return {
|
|
@@ -27,7 +34,10 @@ function buildService(clientOptions, checkNumberPreconditions, routing) {
|
|
|
27
34
|
updateNumber: (0, updateNumber_1.updateNumber)(client, checkNumberPreconditions, routing, checkRefs),
|
|
28
35
|
getNumber: (0, getNumber_1.getNumber)(client),
|
|
29
36
|
listNumbers: (0, listNumbers_1.listNumbers)(client),
|
|
30
|
-
deleteNumber: (0, operations_1.deleteNumber)(client)
|
|
37
|
+
deleteNumber: (0, operations_1.deleteNumber)(client),
|
|
38
|
+
linkTwilio: (0, linkTwilio_1.linkTwilio)(Object.assign({ numbers: client, trunks: new sdk_1.default.Trunks(clientOptions), credentials: new sdk_1.default.Credentials(clientOptions), checkNumberPreconditions,
|
|
39
|
+
routing,
|
|
40
|
+
checkRefs }, twilioLink))
|
|
31
41
|
}
|
|
32
42
|
};
|
|
33
43
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { assignTwilioNumberToTrunk, GrpcErrorMessage, NumberPreconditionsCheck, provisionTwilioTrunk, removeOtherTwilioSipCredentials } from "@fonoster/common";
|
|
2
|
+
import { CredentialsApi, NumbersApi, TrunkApi } from "@fonoster/types";
|
|
3
|
+
import { CheckRefs } from "../resources/checkRefs";
|
|
4
|
+
import { NumberRouting } from "./engineRouting";
|
|
5
|
+
type TwilioLink = {
|
|
6
|
+
provisionTwilioTrunk: typeof provisionTwilioTrunk;
|
|
7
|
+
assignTwilioNumberToTrunk: typeof assignTwilioNumberToTrunk;
|
|
8
|
+
removeOtherTwilioSipCredentials: typeof removeOtherTwilioSipCredentials;
|
|
9
|
+
};
|
|
10
|
+
type LinkTwilioDeps = {
|
|
11
|
+
numbers: NumbersApi;
|
|
12
|
+
trunks: TrunkApi;
|
|
13
|
+
credentials: CredentialsApi;
|
|
14
|
+
checkNumberPreconditions: NumberPreconditionsCheck;
|
|
15
|
+
routing?: NumberRouting;
|
|
16
|
+
checkRefs?: CheckRefs;
|
|
17
|
+
/** Where carriers reach this install's trunks (`pstn.<domain>`). */
|
|
18
|
+
pstnRoot: string;
|
|
19
|
+
/** The install's public signaling addresses; only IPv4 literals are allowed in Twilio. */
|
|
20
|
+
signalingAddresses: string[];
|
|
21
|
+
twilio?: TwilioLink;
|
|
22
|
+
};
|
|
23
|
+
declare function linkTwilio(deps: LinkTwilioDeps): (call: {
|
|
24
|
+
request: unknown;
|
|
25
|
+
}, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
|
|
26
|
+
export { linkTwilio, LinkTwilioDeps };
|
|
@@ -0,0 +1,228 @@
|
|
|
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.linkTwilio = linkTwilio;
|
|
16
|
+
/**
|
|
17
|
+
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
18
|
+
* http://github.com/fonoster/fonoster
|
|
19
|
+
*
|
|
20
|
+
* This file is part of Fonoster
|
|
21
|
+
*
|
|
22
|
+
* Licensed under the MIT License (the "License");
|
|
23
|
+
* you may not use this file except in compliance with
|
|
24
|
+
* the License. You may obtain a copy of the License at
|
|
25
|
+
*
|
|
26
|
+
* https://opensource.org/licenses/MIT
|
|
27
|
+
*
|
|
28
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
29
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
30
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
31
|
+
* See the License for the specific language governing permissions and
|
|
32
|
+
* limitations under the License.
|
|
33
|
+
*/
|
|
34
|
+
const node_crypto_1 = require("node:crypto");
|
|
35
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
36
|
+
const common_1 = require("@fonoster/common");
|
|
37
|
+
const logger_1 = require("@fonoster/logger");
|
|
38
|
+
const types_1 = require("@fonoster/types");
|
|
39
|
+
const grpc_js_1 = require("@grpc/grpc-js");
|
|
40
|
+
const phone_1 = require("phone");
|
|
41
|
+
const checkRefs_1 = require("../resources/checkRefs");
|
|
42
|
+
const paginationUtils_1 = require("../resources/paginationUtils");
|
|
43
|
+
const operations_1 = require("../trunks/operations");
|
|
44
|
+
const convertToRoutrNumber_1 = require("./convertToRoutrNumber");
|
|
45
|
+
const twilioLinkErrorToGrpc_1 = require("./twilioLinkErrorToGrpc");
|
|
46
|
+
const logger = (0, logger_1.getLogger)({ service: "sipnet", filePath: __filename });
|
|
47
|
+
const TWILIO_PSTN_URI_BASE = "pstn.twilio.com";
|
|
48
|
+
const PAGE_SIZE = 100;
|
|
49
|
+
const PASSWORD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
50
|
+
const isIpv4Entry = (address) => {
|
|
51
|
+
const [ip, prefix, ...rest] = address.split("/");
|
|
52
|
+
return (rest.length === 0 &&
|
|
53
|
+
node_net_1.default.isIPv4(ip) &&
|
|
54
|
+
(prefix === undefined || (/^\d{1,2}$/.test(prefix) && +prefix <= 32)));
|
|
55
|
+
};
|
|
56
|
+
function generatePassword() {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const password = Array.from({ length: 24 }, () => PASSWORD_ALPHABET[(0, node_crypto_1.randomInt)(PASSWORD_ALPHABET.length)]).join("");
|
|
59
|
+
if (/[A-Z]/.test(password) &&
|
|
60
|
+
/[a-z]/.test(password) &&
|
|
61
|
+
/\d/.test(password)) {
|
|
62
|
+
return password;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The caller's first resource that matches, through every page Routr has.
|
|
67
|
+
function findOwned(list, accessKeyId, match) {
|
|
68
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
let pageToken;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const { items = [], nextPageToken } = yield list({
|
|
72
|
+
pageSize: PAGE_SIZE,
|
|
73
|
+
pageToken
|
|
74
|
+
});
|
|
75
|
+
const found = (0, paginationUtils_1.filterByAccessKeyId)(items, accessKeyId).find(match);
|
|
76
|
+
if (found || !nextPageToken || items.length < PAGE_SIZE) {
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
pageToken = nextPageToken;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function linkTwilio(deps) {
|
|
84
|
+
const { numbers, trunks, credentials, checkNumberPreconditions, routing, checkRefs, pstnRoot, twilio = {
|
|
85
|
+
provisionTwilioTrunk: common_1.provisionTwilioTrunk,
|
|
86
|
+
assignTwilioNumberToTrunk: common_1.assignTwilioNumberToTrunk,
|
|
87
|
+
removeOtherTwilioSipCredentials: common_1.removeOtherTwilioSipCredentials
|
|
88
|
+
} } = deps;
|
|
89
|
+
const aclEntries = deps.signalingAddresses.filter(isIpv4Entry);
|
|
90
|
+
const fn = (call, callback) => __awaiter(this, void 0, void 0, function* () {
|
|
91
|
+
const { request } = call;
|
|
92
|
+
const { phoneNumber, appRef, agentAor } = request;
|
|
93
|
+
const twilioCredentials = request.credentials;
|
|
94
|
+
const accessKeyId = (0, common_1.getAccessKeyIdFromCall)(call);
|
|
95
|
+
const label = accessKeyId.toLowerCase();
|
|
96
|
+
const inboundUri = `${label}.${pstnRoot}`;
|
|
97
|
+
const telUrl = `tel:${phoneNumber}`;
|
|
98
|
+
const context = {
|
|
99
|
+
accountSid: twilioCredentials.accountSid,
|
|
100
|
+
phoneNumber,
|
|
101
|
+
label
|
|
102
|
+
};
|
|
103
|
+
// Never the request itself: it carries the Twilio secret.
|
|
104
|
+
logger.verbose("call to linkTwilio", {
|
|
105
|
+
phoneNumber,
|
|
106
|
+
credentialKind: "authToken" in twilioCredentials && twilioCredentials.authToken
|
|
107
|
+
? "authToken"
|
|
108
|
+
: "apiKey"
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
// Validates that the appRef exists in the caller's workspace (RV-B4)
|
|
112
|
+
yield checkNumberPreconditions({ appRef, accessKeyId });
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// The contract reports an app outside the workspace as NOT_FOUND, with the usual message.
|
|
116
|
+
if (e instanceof common_1.GrpcError && e.code === grpc_js_1.status.INVALID_ARGUMENT) {
|
|
117
|
+
callback({ code: grpc_js_1.status.NOT_FOUND, message: e.message });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
yield (checkRefs === null || checkRefs === void 0 ? void 0 : checkRefs(accessKeyId, (0, checkRefs_1.numberRefs)(request)));
|
|
123
|
+
if (aclEntries.length === 0) {
|
|
124
|
+
callback({
|
|
125
|
+
code: grpc_js_1.status.FAILED_PRECONDITION,
|
|
126
|
+
message: "This install has no public SIP address to allow in Twilio"
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const sipCredential = {
|
|
131
|
+
username: `fonoster-${(0, node_crypto_1.randomBytes)(4).toString("hex")}`,
|
|
132
|
+
password: generatePassword()
|
|
133
|
+
};
|
|
134
|
+
let twilioTrunkSid;
|
|
135
|
+
try {
|
|
136
|
+
({ twilioTrunkSid } = yield twilio.provisionTwilioTrunk(twilioCredentials, {
|
|
137
|
+
phoneNumber,
|
|
138
|
+
label,
|
|
139
|
+
originationUri: `sip:${inboundUri}`,
|
|
140
|
+
aclEntries,
|
|
141
|
+
sipCredential
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
callback((0, twilioLinkErrorToGrpc_1.twilioLinkErrorToGrpc)(e, context));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const { ref: credentialsRef } = yield credentials.createCredentials(Object.assign(Object.assign({ name: `Twilio ${label}` }, sipCredential), { extended: { accessKeyId } }));
|
|
149
|
+
const trunk = yield findOwned((r) => trunks.listTrunks(r), accessKeyId, (t) => t.inboundUri === inboundUri);
|
|
150
|
+
let trunkRef;
|
|
151
|
+
if (trunk) {
|
|
152
|
+
yield trunks.updateTrunk((0, operations_1.mergeTrunkUpdate)({ ref: trunk.ref, outboundCredentialsRef: credentialsRef }, trunk));
|
|
153
|
+
trunkRef = trunk.ref;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
({ ref: trunkRef } = yield trunks.createTrunk({
|
|
157
|
+
name: "Twilio Trunk",
|
|
158
|
+
inboundUri,
|
|
159
|
+
sendRegister: false,
|
|
160
|
+
outboundCredentialsRef: credentialsRef,
|
|
161
|
+
uris: [
|
|
162
|
+
{
|
|
163
|
+
host: `${label}.${TWILIO_PSTN_URI_BASE}`,
|
|
164
|
+
port: 5060,
|
|
165
|
+
transport: types_1.Transport.UDP,
|
|
166
|
+
enabled: true,
|
|
167
|
+
weight: 10,
|
|
168
|
+
priority: 10
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
extended: { accessKeyId }
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
const number = yield findOwned((r) => numbers.listNumbers(r), accessKeyId, (n) => n.telUrl === telUrl);
|
|
175
|
+
const appPeerAor = routing && appRef ? yield routing.aorFor({ appRef, telUrl }) : undefined;
|
|
176
|
+
let ref;
|
|
177
|
+
if (number) {
|
|
178
|
+
yield numbers.updateNumber((0, convertToRoutrNumber_1.convertToRoutrNumberUpdate)({
|
|
179
|
+
ref: number.ref,
|
|
180
|
+
name: request.name || number.name,
|
|
181
|
+
trunkRef,
|
|
182
|
+
appRef,
|
|
183
|
+
agentAor
|
|
184
|
+
}, appPeerAor));
|
|
185
|
+
ref = number.ref;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const { countryIso2, countryIso3 } = (0, phone_1.phone)(phoneNumber);
|
|
189
|
+
({ ref } = yield numbers.createNumber((0, convertToRoutrNumber_1.convertToRoutrNumber)({
|
|
190
|
+
name: request.name || phoneNumber,
|
|
191
|
+
telUrl,
|
|
192
|
+
city: "unknown",
|
|
193
|
+
country: countryIso3,
|
|
194
|
+
countryIsoCode: countryIso2,
|
|
195
|
+
trunkRef,
|
|
196
|
+
appRef,
|
|
197
|
+
agentAor
|
|
198
|
+
}, accessKeyId, appPeerAor)));
|
|
199
|
+
}
|
|
200
|
+
// The last write to Twilio: until it succeeds, the number keeps its previous routing.
|
|
201
|
+
try {
|
|
202
|
+
yield twilio.assignTwilioNumberToTrunk(twilioCredentials, {
|
|
203
|
+
phoneNumber,
|
|
204
|
+
twilioTrunkSid
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
callback((0, twilioLinkErrorToGrpc_1.twilioLinkErrorToGrpc)(e, context));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
yield twilio.removeOtherTwilioSipCredentials(twilioCredentials, {
|
|
213
|
+
label,
|
|
214
|
+
keepUsername: sipCredential.username
|
|
215
|
+
});
|
|
216
|
+
if (trunk === null || trunk === void 0 ? void 0 : trunk.outboundCredentialsRef) {
|
|
217
|
+
yield credentials.deleteCredentials(trunk.outboundCredentialsRef);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (_a) {
|
|
221
|
+
logger.warn("could not remove the previous SIP credentials", {
|
|
222
|
+
phoneNumber
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
callback(null, { ref, trunkRef });
|
|
226
|
+
});
|
|
227
|
+
return (0, common_1.withErrorHandlingAndValidation)(fn, common_1.Validators.linkTwilioRequestSchema);
|
|
228
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
* The gRPC error for a failed Twilio call. Clients place errors by these messages, so they are
|
|
22
|
+
* part of the contract. Twilio's own text never reaches the caller or the logs, and anything
|
|
23
|
+
* unrecognized is reported as Twilio being unavailable.
|
|
24
|
+
*/
|
|
25
|
+
declare function twilioLinkErrorToGrpc(error: unknown, context: {
|
|
26
|
+
accountSid: string;
|
|
27
|
+
phoneNumber: string;
|
|
28
|
+
label: string;
|
|
29
|
+
}): GrpcErrorMessage;
|
|
30
|
+
export { twilioLinkErrorToGrpc };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.twilioLinkErrorToGrpc = twilioLinkErrorToGrpc;
|
|
4
|
+
/**
|
|
5
|
+
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
6
|
+
* http://github.com/fonoster/fonoster
|
|
7
|
+
*
|
|
8
|
+
* This file is part of Fonoster
|
|
9
|
+
*
|
|
10
|
+
* Licensed under the MIT License (the "License");
|
|
11
|
+
* you may not use this file except in compliance with
|
|
12
|
+
* the License. You may obtain a copy of the License at
|
|
13
|
+
*
|
|
14
|
+
* https://opensource.org/licenses/MIT
|
|
15
|
+
*
|
|
16
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
* See the License for the specific language governing permissions and
|
|
20
|
+
* limitations under the License.
|
|
21
|
+
*/
|
|
22
|
+
const common_1 = require("@fonoster/common");
|
|
23
|
+
const grpc_js_1 = require("@grpc/grpc-js");
|
|
24
|
+
const UNAVAILABLE = {
|
|
25
|
+
code: grpc_js_1.status.UNAVAILABLE,
|
|
26
|
+
message: "Twilio is unavailable; try again"
|
|
27
|
+
};
|
|
28
|
+
// What a key without the permission for a Twilio call can't do, by the call's prefix
|
|
29
|
+
function missingPermission(step = "") {
|
|
30
|
+
if (step.startsWith("sip.")) {
|
|
31
|
+
return "manage SIP IP access control lists and credential lists";
|
|
32
|
+
}
|
|
33
|
+
if (step.startsWith("incomingPhoneNumbers.")) {
|
|
34
|
+
return "read or update phone numbers";
|
|
35
|
+
}
|
|
36
|
+
return "manage Elastic SIP Trunking";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The gRPC error for a failed Twilio call. Clients place errors by these messages, so they are
|
|
40
|
+
* part of the contract. Twilio's own text never reaches the caller or the logs, and anything
|
|
41
|
+
* unrecognized is reported as Twilio being unavailable.
|
|
42
|
+
*/
|
|
43
|
+
function twilioLinkErrorToGrpc(error, context) {
|
|
44
|
+
if (!(error instanceof common_1.TwilioLinkError)) {
|
|
45
|
+
return UNAVAILABLE;
|
|
46
|
+
}
|
|
47
|
+
const failedPrecondition = (message) => ({
|
|
48
|
+
code: grpc_js_1.status.FAILED_PRECONDITION,
|
|
49
|
+
message
|
|
50
|
+
});
|
|
51
|
+
switch (error.reason) {
|
|
52
|
+
case "INVALID_CREDENTIALS":
|
|
53
|
+
return { code: grpc_js_1.status.INVALID_ARGUMENT, message: error.message };
|
|
54
|
+
case "CREDENTIALS_REFUSED":
|
|
55
|
+
return failedPrecondition("Twilio didn't accept these credentials");
|
|
56
|
+
case "PERMISSION_MISSING":
|
|
57
|
+
return failedPrecondition(`This Twilio key can't ${missingPermission(error.step)}. Twilio's Restricted keys can't link numbers; use a Standard API key or the Auth Token`);
|
|
58
|
+
case "NUMBER_NOT_FOUND":
|
|
59
|
+
return failedPrecondition(`${context.phoneNumber} isn't in Twilio account AC…${context.accountSid.slice(-4)}`);
|
|
60
|
+
case "DOMAIN_TAKEN":
|
|
61
|
+
return failedPrecondition(`Another Twilio account already uses ${context.label}.pstn.twilio.com; link from the Twilio account this workspace linked first`);
|
|
62
|
+
default:
|
|
63
|
+
return UNAVAILABLE;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonoster/sipnet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Routr-based SIP stack",
|
|
5
5
|
"author": "Pedro Sanders <psanders@fonoster.com>",
|
|
6
6
|
"homepage": "https://github.com/fonoster/fonoster#readme",
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
"fonoster": "./dist/index.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@fonoster/common": "^0.
|
|
23
|
-
"@fonoster/identity": "^0.
|
|
24
|
-
"@fonoster/logger": "^0.
|
|
25
|
-
"@fonoster/types": "^0.
|
|
22
|
+
"@fonoster/common": "^0.38.0",
|
|
23
|
+
"@fonoster/identity": "^0.38.0",
|
|
24
|
+
"@fonoster/logger": "^0.38.0",
|
|
25
|
+
"@fonoster/types": "^0.38.0",
|
|
26
26
|
"@grpc/grpc-js": "~1.14.6",
|
|
27
27
|
"@routr/sdk": "2.24.0",
|
|
28
|
+
"phone": "^3.1.72",
|
|
28
29
|
"zod": "^3.23.8"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
@@ -40,5 +41,5 @@
|
|
|
40
41
|
"bugs": {
|
|
41
42
|
"url": "https://github.com/fonoster/fonoster/issues"
|
|
42
43
|
},
|
|
43
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "111977a48870cf2b4e29a5ed644b95195217384f"
|
|
44
45
|
}
|