ring-client-api 10.0.0-beta.8 → 11.0.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.
- package/CHANGELOG.md +64 -0
- package/lib/api/api.js +79 -96
- package/lib/api/device-data.js +20 -31
- package/lib/api/location.js +125 -157
- package/lib/api/refresh-token.js +23 -36
- package/lib/api/rest-client.js +159 -165
- package/lib/api/ring-camera.js +121 -157
- package/lib/api/ring-chime.js +32 -51
- package/lib/api/ring-device.js +4 -1
- package/lib/api/streaming/peer-connection.d.ts +3 -3
- package/lib/api/streaming/peer-connection.js +12 -27
- package/lib/api/streaming/ring-edge-connection.js +56 -65
- package/lib/api/streaming/simple-webrtc-session.js +27 -42
- package/lib/api/streaming/streaming-connection-base.d.ts +1 -1
- package/lib/api/streaming/streaming-connection-base.js +12 -25
- package/lib/api/streaming/streaming-session.d.ts +1 -1
- package/lib/api/streaming/streaming-session.js +56 -73
- package/lib/api/streaming/webrtc-connection.js +22 -30
- package/lib/api/util.js +35 -39
- package/package.json +15 -15
package/lib/api/rest-client.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
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
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
4
|
};
|
|
@@ -40,37 +31,37 @@ function appApi(path) {
|
|
|
40
31
|
return appApiBaseUrl + path;
|
|
41
32
|
}
|
|
42
33
|
exports.appApi = appApi;
|
|
43
|
-
function requestWithRetry(requestOptions) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
34
|
+
async function requestWithRetry(requestOptions) {
|
|
35
|
+
try {
|
|
36
|
+
const options = {
|
|
37
|
+
...defaultRequestOptions,
|
|
38
|
+
...requestOptions,
|
|
39
|
+
}, { headers, body } = (await (0, got_1.default)(options)), data = body;
|
|
40
|
+
if (data !== null && typeof data === 'object') {
|
|
41
|
+
if (headers.date) {
|
|
42
|
+
data.responseTimestamp = new Date(headers.date).getTime();
|
|
43
|
+
}
|
|
44
|
+
if (headers['x-time-millis']) {
|
|
45
|
+
data.timeMillis = Number(headers['x-time-millis']);
|
|
54
46
|
}
|
|
55
|
-
return data;
|
|
56
47
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(0, util_1.
|
|
64
|
-
yield (0, util_1.delay)(5000);
|
|
65
|
-
return requestWithRetry(requestOptions);
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
if (!e.response && !requestOptions.allowNoResponse) {
|
|
52
|
+
(0, util_1.logError)(`Failed to reach Ring server at ${requestOptions.url}. ${e.message}. Trying again in 5 seconds...`);
|
|
53
|
+
if (e.message.includes('NGHTTP2_ENHANCE_YOUR_CALM')) {
|
|
54
|
+
(0, util_1.logError)(`There is a known issue with your current NodeJS version (${process.version}). Please see https://github.com/dgreif/ring/wiki/NGHTTP2_ENHANCE_YOUR_CALM-Error for details`);
|
|
66
55
|
}
|
|
67
|
-
|
|
56
|
+
(0, util_1.logDebug)(e);
|
|
57
|
+
await (0, util_1.delay)(5000);
|
|
58
|
+
return requestWithRetry(requestOptions);
|
|
68
59
|
}
|
|
69
|
-
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
70
62
|
}
|
|
71
63
|
class RingRestClient {
|
|
72
64
|
constructor(authOptions) {
|
|
73
|
-
var _a;
|
|
74
65
|
this.authOptions = authOptions;
|
|
75
66
|
this.timeouts = [];
|
|
76
67
|
this.sessionPromise = undefined;
|
|
@@ -78,7 +69,7 @@ class RingRestClient {
|
|
|
78
69
|
this.onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
|
|
79
70
|
this.baseSessionMetadata = {
|
|
80
71
|
api_version: apiVersion,
|
|
81
|
-
device_model:
|
|
72
|
+
device_model: this.authOptions.controlCenterDisplayName ?? 'ring-client-api',
|
|
82
73
|
};
|
|
83
74
|
this.refreshToken =
|
|
84
75
|
'refreshToken' in this.authOptions
|
|
@@ -126,95 +117,95 @@ class RingRestClient {
|
|
|
126
117
|
}
|
|
127
118
|
throw new Error('Refresh token is not valid. Unable to authenticate with Ring servers. See https://github.com/dgreif/ring/wiki/Refresh-Tokens');
|
|
128
119
|
}
|
|
129
|
-
getAuth(twoFactorAuthCode) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
url: 'https://oauth.ring.com/oauth/token',
|
|
135
|
-
json: Object.assign({ client_id: 'ring_official_android', scope: 'client' }, grantData),
|
|
136
|
-
method: 'POST',
|
|
137
|
-
headers: {
|
|
138
|
-
'2fa-support': 'true',
|
|
139
|
-
'2fa-code': twoFactorAuthCode || '',
|
|
140
|
-
hardware_id: yield this.hardwareIdPromise,
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
this.onRefreshTokenUpdated.next({
|
|
144
|
-
oldRefreshToken: this.refreshToken,
|
|
145
|
-
newRefreshToken: response.refresh_token,
|
|
146
|
-
});
|
|
147
|
-
this.refreshToken = response.refresh_token;
|
|
148
|
-
return response;
|
|
149
|
-
}
|
|
150
|
-
catch (requestError) {
|
|
151
|
-
if (grantData.refresh_token) {
|
|
152
|
-
// failed request with refresh token
|
|
153
|
-
this.refreshToken = undefined;
|
|
154
|
-
(0, util_1.logError)(requestError);
|
|
155
|
-
return this.getAuth();
|
|
156
|
-
}
|
|
157
|
-
const response = requestError.response || {}, responseData = response.body || {}, responseError = 'error' in responseData && typeof responseData.error === 'string'
|
|
158
|
-
? responseData.error
|
|
159
|
-
: '';
|
|
160
|
-
if (response.statusCode === 412 || // need 2fa code
|
|
161
|
-
(response.statusCode === 400 &&
|
|
162
|
-
responseError.startsWith('Verification Code')) // invalid 2fa code entered
|
|
163
|
-
) {
|
|
164
|
-
this.using2fa = true;
|
|
165
|
-
if ('tsv_state' in responseData) {
|
|
166
|
-
const { tsv_state, phone } = responseData, prompt = tsv_state === 'totp'
|
|
167
|
-
? 'from your authenticator app'
|
|
168
|
-
: `sent to ${phone} via ${tsv_state}`;
|
|
169
|
-
this.promptFor2fa = `Please enter the code ${prompt}`;
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
this.promptFor2fa = 'Please enter the code sent to your text/email';
|
|
173
|
-
}
|
|
174
|
-
throw new Error('Your Ring account is configured to use 2-factor authentication (2fa). See https://github.com/dgreif/ring/wiki/Refresh-Tokens for details.');
|
|
175
|
-
}
|
|
176
|
-
const authTypeMessage = 'refreshToken' in this.authOptions
|
|
177
|
-
? 'refresh token is'
|
|
178
|
-
: 'email and password are', errorMessage = 'Failed to fetch oauth token from Ring. ' +
|
|
179
|
-
('error_description' in responseData &&
|
|
180
|
-
responseData.error_description ===
|
|
181
|
-
'too many requests from dependency service'
|
|
182
|
-
? 'You have requested too many 2fa codes. Ring limits 2fa to 10 codes within 10 minutes. Please try again in 10 minutes.'
|
|
183
|
-
: `Verify that your ${authTypeMessage} correct.`) +
|
|
184
|
-
` (error: ${responseError})`;
|
|
185
|
-
(0, util_1.logError)(requestError.response || requestError);
|
|
186
|
-
(0, util_1.logError)(errorMessage);
|
|
187
|
-
throw new Error(errorMessage);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
fetchNewSession(authToken) {
|
|
192
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
193
|
-
return requestWithRetry({
|
|
194
|
-
url: clientApi('session'),
|
|
120
|
+
async getAuth(twoFactorAuthCode) {
|
|
121
|
+
const grantData = this.getGrantData(twoFactorAuthCode);
|
|
122
|
+
try {
|
|
123
|
+
const response = await requestWithRetry({
|
|
124
|
+
url: 'https://oauth.ring.com/oauth/token',
|
|
195
125
|
json: {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
os: 'android', // can use android, ios, ring-site, windows for sure
|
|
200
|
-
},
|
|
126
|
+
client_id: 'ring_official_android',
|
|
127
|
+
scope: 'client',
|
|
128
|
+
...grantData,
|
|
201
129
|
},
|
|
202
130
|
method: 'POST',
|
|
203
131
|
headers: {
|
|
204
|
-
|
|
132
|
+
'2fa-support': 'true',
|
|
133
|
+
'2fa-code': twoFactorAuthCode || '',
|
|
134
|
+
hardware_id: await this.hardwareIdPromise,
|
|
205
135
|
},
|
|
206
136
|
});
|
|
137
|
+
this.onRefreshTokenUpdated.next({
|
|
138
|
+
oldRefreshToken: this.refreshToken,
|
|
139
|
+
newRefreshToken: response.refresh_token,
|
|
140
|
+
});
|
|
141
|
+
this.refreshToken = response.refresh_token;
|
|
142
|
+
return response;
|
|
143
|
+
}
|
|
144
|
+
catch (requestError) {
|
|
145
|
+
if (grantData.refresh_token) {
|
|
146
|
+
// failed request with refresh token
|
|
147
|
+
this.refreshToken = undefined;
|
|
148
|
+
(0, util_1.logError)(requestError);
|
|
149
|
+
return this.getAuth();
|
|
150
|
+
}
|
|
151
|
+
const response = requestError.response || {}, responseData = response.body || {}, responseError = 'error' in responseData && typeof responseData.error === 'string'
|
|
152
|
+
? responseData.error
|
|
153
|
+
: '';
|
|
154
|
+
if (response.statusCode === 412 || // need 2fa code
|
|
155
|
+
(response.statusCode === 400 &&
|
|
156
|
+
responseError.startsWith('Verification Code')) // invalid 2fa code entered
|
|
157
|
+
) {
|
|
158
|
+
this.using2fa = true;
|
|
159
|
+
if ('tsv_state' in responseData) {
|
|
160
|
+
const { tsv_state, phone } = responseData, prompt = tsv_state === 'totp'
|
|
161
|
+
? 'from your authenticator app'
|
|
162
|
+
: `sent to ${phone} via ${tsv_state}`;
|
|
163
|
+
this.promptFor2fa = `Please enter the code ${prompt}`;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
this.promptFor2fa = 'Please enter the code sent to your text/email';
|
|
167
|
+
}
|
|
168
|
+
throw new Error('Your Ring account is configured to use 2-factor authentication (2fa). See https://github.com/dgreif/ring/wiki/Refresh-Tokens for details.');
|
|
169
|
+
}
|
|
170
|
+
const authTypeMessage = 'refreshToken' in this.authOptions
|
|
171
|
+
? 'refresh token is'
|
|
172
|
+
: 'email and password are', errorMessage = 'Failed to fetch oauth token from Ring. ' +
|
|
173
|
+
('error_description' in responseData &&
|
|
174
|
+
responseData.error_description ===
|
|
175
|
+
'too many requests from dependency service'
|
|
176
|
+
? 'You have requested too many 2fa codes. Ring limits 2fa to 10 codes within 10 minutes. Please try again in 10 minutes.'
|
|
177
|
+
: `Verify that your ${authTypeMessage} correct.`) +
|
|
178
|
+
` (error: ${responseError})`;
|
|
179
|
+
(0, util_1.logError)(requestError.response || requestError);
|
|
180
|
+
(0, util_1.logError)(errorMessage);
|
|
181
|
+
throw new Error(errorMessage);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async fetchNewSession(authToken) {
|
|
185
|
+
return requestWithRetry({
|
|
186
|
+
url: clientApi('session'),
|
|
187
|
+
json: {
|
|
188
|
+
device: {
|
|
189
|
+
hardware_id: await this.hardwareIdPromise,
|
|
190
|
+
metadata: this.baseSessionMetadata,
|
|
191
|
+
os: 'android', // can use android, ios, ring-site, windows for sure
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
authorization: `Bearer ${authToken.access_token}`,
|
|
197
|
+
},
|
|
207
198
|
});
|
|
208
199
|
}
|
|
209
200
|
getSession() {
|
|
210
|
-
return this.authPromise.then((authToken) =>
|
|
201
|
+
return this.authPromise.then(async (authToken) => {
|
|
211
202
|
try {
|
|
212
|
-
return
|
|
203
|
+
return await this.fetchNewSession(authToken);
|
|
213
204
|
}
|
|
214
205
|
catch (e) {
|
|
215
206
|
const response = e.response || {};
|
|
216
207
|
if (response.statusCode === 401) {
|
|
217
|
-
|
|
208
|
+
await this.refreshAuth();
|
|
218
209
|
return this.getSession();
|
|
219
210
|
}
|
|
220
211
|
if (response.statusCode === 429) {
|
|
@@ -222,78 +213,81 @@ class RingRestClient {
|
|
|
222
213
|
? 200
|
|
223
214
|
: Number.parseInt(retryAfter, 10);
|
|
224
215
|
(0, util_1.logError)(`Session response rate limited. Waiting to retry after ${waitSeconds} seconds`);
|
|
225
|
-
|
|
216
|
+
await (0, util_1.delay)((waitSeconds + 1) * 1000);
|
|
226
217
|
(0, util_1.logInfo)('Retrying session request');
|
|
227
218
|
return this.getSession();
|
|
228
219
|
}
|
|
229
220
|
throw e;
|
|
230
221
|
}
|
|
231
|
-
}));
|
|
232
|
-
}
|
|
233
|
-
refreshAuth() {
|
|
234
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
-
this.clearPreviousAuth();
|
|
236
|
-
yield this.authPromise;
|
|
237
222
|
});
|
|
238
223
|
}
|
|
224
|
+
async refreshAuth() {
|
|
225
|
+
this.clearPreviousAuth();
|
|
226
|
+
await this.authPromise;
|
|
227
|
+
}
|
|
239
228
|
refreshSession() {
|
|
240
229
|
this.sessionPromise = this.getSession();
|
|
241
230
|
}
|
|
242
|
-
request(options) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
231
|
+
async request(options) {
|
|
232
|
+
const hardwareId = await this.hardwareIdPromise, url = options.url, initialSessionPromise = this.sessionPromise;
|
|
233
|
+
try {
|
|
234
|
+
await initialSessionPromise;
|
|
235
|
+
const authTokenResponse = await this.authPromise;
|
|
236
|
+
return await requestWithRetry({
|
|
237
|
+
...options,
|
|
238
|
+
headers: {
|
|
239
|
+
...options.headers,
|
|
240
|
+
authorization: `Bearer ${authTokenResponse.access_token}`,
|
|
241
|
+
hardware_id: hardwareId,
|
|
242
|
+
'User-Agent': 'android:com.ringapp',
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
const response = e.response || {};
|
|
248
|
+
if (response.statusCode === 401) {
|
|
249
|
+
await this.refreshAuth();
|
|
250
|
+
return this.request(options);
|
|
250
251
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
if (response.statusCode === 504) {
|
|
253
|
+
// Gateway Timeout. These should be recoverable, but wait a few seconds just to be on the safe side
|
|
254
|
+
await (0, util_1.delay)(5000);
|
|
255
|
+
return this.request(options);
|
|
256
|
+
}
|
|
257
|
+
if (response.statusCode === 404 &&
|
|
258
|
+
response.body &&
|
|
259
|
+
Array.isArray(response.body.errors)) {
|
|
260
|
+
const errors = response.body.errors, errorText = errors
|
|
261
|
+
.map((code) => ringErrorCodes[code])
|
|
262
|
+
.filter((x) => x)
|
|
263
|
+
.join(', ');
|
|
264
|
+
if (errorText) {
|
|
265
|
+
(0, util_1.logError)(`http request failed. ${url} returned errors: (${errorText}). Trying again in 20 seconds`);
|
|
266
|
+
await (0, util_1.delay)(20000);
|
|
260
267
|
return this.request(options);
|
|
261
268
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
(0, util_1.logError)(`http request failed. ${url} returned errors: (${errorText}). Trying again in 20 seconds`);
|
|
271
|
-
yield (0, util_1.delay)(20000);
|
|
272
|
-
return this.request(options);
|
|
273
|
-
}
|
|
274
|
-
(0, util_1.logError)(`http request failed. ${url} returned unknown errors: (${(0, util_1.stringify)(errors)}).`);
|
|
275
|
-
}
|
|
276
|
-
if (response.statusCode === 404 && url.startsWith(clientApiBaseUrl)) {
|
|
277
|
-
(0, util_1.logError)('404 from endpoint ' + url);
|
|
278
|
-
if ((_b = (_a = response.body) === null || _a === void 0 ? void 0 : _a.error) === null || _b === void 0 ? void 0 : _b.includes(hardwareId)) {
|
|
279
|
-
(0, util_1.logError)('Session hardware_id not found. Creating a new session and trying again.');
|
|
280
|
-
if (this.sessionPromise === initialSessionPromise) {
|
|
281
|
-
this.refreshSession();
|
|
282
|
-
}
|
|
283
|
-
return this.request(options);
|
|
269
|
+
(0, util_1.logError)(`http request failed. ${url} returned unknown errors: (${(0, util_1.stringify)(errors)}).`);
|
|
270
|
+
}
|
|
271
|
+
if (response.statusCode === 404 && url.startsWith(clientApiBaseUrl)) {
|
|
272
|
+
(0, util_1.logError)('404 from endpoint ' + url);
|
|
273
|
+
if (response.body?.error?.includes(hardwareId)) {
|
|
274
|
+
(0, util_1.logError)('Session hardware_id not found. Creating a new session and trying again.');
|
|
275
|
+
if (this.sessionPromise === initialSessionPromise) {
|
|
276
|
+
this.refreshSession();
|
|
284
277
|
}
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
if (response.statusCode) {
|
|
288
|
-
(0, util_1.logError)(`Request to ${url} failed with status ${response.statusCode}. Response body: ${(0, util_1.stringify)(response.body)}`);
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
(0, util_1.logError)(`Request to ${url} failed:`);
|
|
292
|
-
(0, util_1.logError)(e);
|
|
278
|
+
return this.request(options);
|
|
293
279
|
}
|
|
294
|
-
throw
|
|
280
|
+
throw new Error('Not found with response: ' + (0, util_1.stringify)(response.body));
|
|
295
281
|
}
|
|
296
|
-
|
|
282
|
+
if (response.statusCode) {
|
|
283
|
+
(0, util_1.logError)(`Request to ${url} failed with status ${response.statusCode}. Response body: ${(0, util_1.stringify)(response.body)}`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
(0, util_1.logError)(`Request to ${url} failed:`);
|
|
287
|
+
(0, util_1.logError)(e);
|
|
288
|
+
}
|
|
289
|
+
throw e;
|
|
290
|
+
}
|
|
297
291
|
}
|
|
298
292
|
getCurrentAuth() {
|
|
299
293
|
return this.authPromise;
|