@stoprocent/noble 1.9.2-16

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 (112) hide show
  1. package/.editorconfig +11 -0
  2. package/.eslintrc.js +25 -0
  3. package/.github/FUNDING.yml +2 -0
  4. package/.github/workflows/fediverse-action.yml +16 -0
  5. package/.github/workflows/nodepackage.yml +77 -0
  6. package/.github/workflows/npm-publish.yml +26 -0
  7. package/.github/workflows/prebuild.yml +65 -0
  8. package/.nycrc.json +4 -0
  9. package/CHANGELOG.md +119 -0
  10. package/LICENSE +20 -0
  11. package/MAINTAINERS.md +1 -0
  12. package/README.md +833 -0
  13. package/assets/noble-logo.png +0 -0
  14. package/assets/noble-logo.svg +13 -0
  15. package/binding.gyp +19 -0
  16. package/codecov.yml +5 -0
  17. package/examples/advertisement-discovery.js +65 -0
  18. package/examples/cache-gatt-discovery.js +198 -0
  19. package/examples/cache-gatt-reconnect.js +164 -0
  20. package/examples/echo.js +104 -0
  21. package/examples/enter-exit.js +78 -0
  22. package/examples/peripheral-explorer-async.js +133 -0
  23. package/examples/peripheral-explorer.js +225 -0
  24. package/examples/pizza/README.md +15 -0
  25. package/examples/pizza/central.js +194 -0
  26. package/examples/pizza/pizza.js +60 -0
  27. package/index.d.ts +203 -0
  28. package/index.js +6 -0
  29. package/lib/characteristic.js +161 -0
  30. package/lib/characteristics.json +449 -0
  31. package/lib/descriptor.js +72 -0
  32. package/lib/descriptors.json +47 -0
  33. package/lib/distributed/bindings.js +326 -0
  34. package/lib/hci-socket/acl-stream.js +60 -0
  35. package/lib/hci-socket/bindings.js +788 -0
  36. package/lib/hci-socket/crypto.js +74 -0
  37. package/lib/hci-socket/gap.js +432 -0
  38. package/lib/hci-socket/gatt.js +809 -0
  39. package/lib/hci-socket/hci-status.json +71 -0
  40. package/lib/hci-socket/hci.js +1264 -0
  41. package/lib/hci-socket/signaling.js +76 -0
  42. package/lib/hci-socket/smp.js +140 -0
  43. package/lib/hci-uart/bindings.js +569 -0
  44. package/lib/hci-uart/hci-serial-parser.js +70 -0
  45. package/lib/hci-uart/hci.js +1336 -0
  46. package/lib/mac/binding.gyp +26 -0
  47. package/lib/mac/bindings.js +11 -0
  48. package/lib/mac/src/ble_manager.h +41 -0
  49. package/lib/mac/src/ble_manager.mm +435 -0
  50. package/lib/mac/src/callbacks.cc +222 -0
  51. package/lib/mac/src/callbacks.h +84 -0
  52. package/lib/mac/src/napi_objc.h +12 -0
  53. package/lib/mac/src/napi_objc.mm +50 -0
  54. package/lib/mac/src/noble_mac.h +34 -0
  55. package/lib/mac/src/noble_mac.mm +264 -0
  56. package/lib/mac/src/objc_cpp.h +26 -0
  57. package/lib/mac/src/objc_cpp.mm +126 -0
  58. package/lib/mac/src/peripheral.h +23 -0
  59. package/lib/manufacture.js +48 -0
  60. package/lib/noble.js +593 -0
  61. package/lib/peripheral.js +219 -0
  62. package/lib/resolve-bindings-web.js +9 -0
  63. package/lib/resolve-bindings.js +44 -0
  64. package/lib/service.js +72 -0
  65. package/lib/services.json +92 -0
  66. package/lib/webbluetooth/bindings.js +368 -0
  67. package/lib/websocket/bindings.js +321 -0
  68. package/lib/win/binding.gyp +23 -0
  69. package/lib/win/bindings.js +11 -0
  70. package/lib/win/src/ble_manager.cc +802 -0
  71. package/lib/win/src/ble_manager.h +77 -0
  72. package/lib/win/src/callbacks.cc +274 -0
  73. package/lib/win/src/callbacks.h +33 -0
  74. package/lib/win/src/napi_winrt.cc +76 -0
  75. package/lib/win/src/napi_winrt.h +12 -0
  76. package/lib/win/src/noble_winrt.cc +308 -0
  77. package/lib/win/src/noble_winrt.h +34 -0
  78. package/lib/win/src/notify_map.cc +62 -0
  79. package/lib/win/src/notify_map.h +50 -0
  80. package/lib/win/src/peripheral.h +23 -0
  81. package/lib/win/src/peripheral_winrt.cc +296 -0
  82. package/lib/win/src/peripheral_winrt.h +82 -0
  83. package/lib/win/src/radio_watcher.cc +125 -0
  84. package/lib/win/src/radio_watcher.h +61 -0
  85. package/lib/win/src/winrt_cpp.cc +82 -0
  86. package/lib/win/src/winrt_cpp.h +11 -0
  87. package/lib/win/src/winrt_guid.cc +12 -0
  88. package/lib/win/src/winrt_guid.h +13 -0
  89. package/misc/nrf52840dk.hex +6921 -0
  90. package/misc/prj.conf +43 -0
  91. package/package.json +96 -0
  92. package/test/lib/characteristic.test.js +791 -0
  93. package/test/lib/descriptor.test.js +249 -0
  94. package/test/lib/distributed/bindings.test.js +918 -0
  95. package/test/lib/hci-socket/acl-stream.test.js +188 -0
  96. package/test/lib/hci-socket/bindings.test.js +1756 -0
  97. package/test/lib/hci-socket/crypto.test.js +55 -0
  98. package/test/lib/hci-socket/gap.test.js +1089 -0
  99. package/test/lib/hci-socket/gatt.test.js +2392 -0
  100. package/test/lib/hci-socket/hci.test.js +1891 -0
  101. package/test/lib/hci-socket/signaling.test.js +94 -0
  102. package/test/lib/hci-socket/smp.test.js +268 -0
  103. package/test/lib/manufacture.test.js +77 -0
  104. package/test/lib/peripheral.test.js +623 -0
  105. package/test/lib/resolve-bindings.test.js +102 -0
  106. package/test/lib/service.test.js +195 -0
  107. package/test/lib/webbluetooth/bindings.test.js +190 -0
  108. package/test/lib/websocket/bindings.test.js +456 -0
  109. package/test/noble.test.js +1565 -0
  110. package/test.js +131 -0
  111. package/with-bindings.js +5 -0
  112. package/ws-slave.js +404 -0
@@ -0,0 +1,74 @@
1
+ const crypto = require('crypto');
2
+
3
+ function r () {
4
+ return crypto.randomBytes(16);
5
+ }
6
+
7
+ function c1 (k, r, pres, preq, iat, ia, rat, ra) {
8
+ const p1 = Buffer.concat([
9
+ iat,
10
+ rat,
11
+ preq,
12
+ pres
13
+ ]);
14
+
15
+ const p2 = Buffer.concat([
16
+ ra,
17
+ ia,
18
+ Buffer.from('00000000', 'hex')
19
+ ]);
20
+
21
+ let res = xor(r, p1);
22
+ res = e(k, res);
23
+ res = xor(res, p2);
24
+ res = e(k, res);
25
+
26
+ return res;
27
+ }
28
+
29
+ function s1 (k, r1, r2) {
30
+ return e(k, Buffer.concat([
31
+ r2.slice(0, 8),
32
+ r1.slice(0, 8)
33
+ ]));
34
+ }
35
+
36
+ function e (key, data) {
37
+ key = swap(key);
38
+ data = swap(data);
39
+
40
+ const cipher = crypto.createCipheriv('aes-128-ecb', key, '');
41
+ cipher.setAutoPadding(false);
42
+
43
+ return swap(Buffer.concat([
44
+ cipher.update(data),
45
+ cipher.final()
46
+ ]));
47
+ }
48
+
49
+ function xor (b1, b2) {
50
+ const result = Buffer.alloc(b1.length);
51
+
52
+ for (let i = 0; i < b1.length; i++) {
53
+ result[i] = b1[i] ^ b2[i];
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ function swap (input) {
60
+ const output = Buffer.alloc(input.length);
61
+
62
+ for (let i = 0; i < output.length; i++) {
63
+ output[i] = input[input.length - i - 1];
64
+ }
65
+
66
+ return output;
67
+ }
68
+
69
+ module.exports = {
70
+ r: r,
71
+ c1: c1,
72
+ s1: s1,
73
+ e: e
74
+ };
@@ -0,0 +1,432 @@
1
+ const debug = require('debug')('gap');
2
+
3
+ const events = require('events');
4
+ const os = require('os');
5
+ const util = require('util');
6
+
7
+ const isChip = os.platform() === 'linux' && os.release().indexOf('-ntc') !== -1;
8
+
9
+ const LE_META_EVENT_TYPE_CONNECTABLE = 0x3;
10
+ const LE_META_EVENT_TYPE_SCAN_RESPONSE = 0x4;
11
+ const LE_META_EVENT_TYPE_SCANNABLE = 0x6;
12
+
13
+ const LE_META_EXTENDED_EVENT_TYPE_CONNECTABLE_MASK = 0x1;
14
+ const LE_META_EXTENDED_EVENT_TYPE_SCANNABLE_MASK = 0x2;
15
+ const LE_META_EXTENDED_EVENT_TYPE_SCAN_RESPONSE_MASK = 0x8;
16
+ const LE_META_EXTENDED_EVENT_TYPE_INCOMPLETE_MASK = 0x20;
17
+
18
+ const Gap = function (hci) {
19
+ this._hci = hci;
20
+
21
+ this._scanState = null;
22
+ this._scanFilterDuplicates = null;
23
+ this._discoveries = {};
24
+
25
+ this._hci.on('error', this.onHciError.bind(this));
26
+ this._hci.on('leScanParametersSet', this.onHciLeScanParametersSet.bind(this));
27
+ this._hci.on('leScanEnableSet', this.onHciLeScanEnableSet.bind(this));
28
+ this._hci.on('leAdvertisingReport', this.onHciLeAdvertisingReport.bind(this));
29
+ this._hci.on(
30
+ 'leExtendedAdvertisingReport',
31
+ this.onHciLeExtendedAdvertisingReport.bind(this)
32
+ );
33
+
34
+ this._hci.on('leScanEnableSetCmd', this.onLeScanEnableSetCmd.bind(this));
35
+ };
36
+
37
+ util.inherits(Gap, events.EventEmitter);
38
+
39
+ Gap.prototype.setScanParameters = function (interval, window) {
40
+ this._hci.setScanParameters(interval, window);
41
+ };
42
+
43
+ Gap.prototype.startScanning = function (allowDuplicates) {
44
+ this._scanState = 'starting';
45
+ this._scanFilterDuplicates = !allowDuplicates;
46
+
47
+ // Always set scan parameters before scanning
48
+ // https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=229737
49
+ // p106 - p107
50
+ this._hci.setScanEnabled(false, true);
51
+ this._hci.setScanParameters();
52
+
53
+ if (isChip) {
54
+ // work around for Next Thing Co. C.H.I.P, always allow duplicates, to get scan response
55
+ this._scanFilterDuplicates = false;
56
+ }
57
+
58
+ this._hci.setScanEnabled(true, this._scanFilterDuplicates);
59
+ };
60
+
61
+ Gap.prototype.stopScanning = function () {
62
+ this._scanState = 'stopping';
63
+
64
+ this._hci.setScanEnabled(false, true);
65
+ };
66
+
67
+ Gap.prototype.onHciError = function (error) {
68
+ console.warn(error); // TODO: Better error handling
69
+ };
70
+
71
+ Gap.prototype.onHciLeScanParametersSet = function () {
72
+ this.emit('scanParametersSet');
73
+ };
74
+
75
+ // Called when receive an event "Command Complete" for "LE Set Scan Enable"
76
+ Gap.prototype.onHciLeScanEnableSet = function (status) {
77
+ // Check the status we got from the command complete function.
78
+ if (status !== 0) {
79
+ // If it is non-zero there was an error, and we should not change
80
+ // our status as a result.
81
+ return;
82
+ }
83
+
84
+ if (this._scanState === 'starting') {
85
+ this._scanState = 'started';
86
+
87
+ this.emit('scanStart', this._scanFilterDuplicates);
88
+ } else if (this._scanState === 'stopping') {
89
+ this._scanState = 'stopped';
90
+
91
+ this.emit('scanStop');
92
+ }
93
+ };
94
+
95
+ // Called when we see the actual command "LE Set Scan Enable"
96
+ Gap.prototype.onLeScanEnableSetCmd = function (enable, filterDuplicates) {
97
+ // Check to see if the new settings differ from what we expect.
98
+ // If we are scanning, then a change happens if the new command stops
99
+ // scanning or if duplicate filtering changes.
100
+ // If we are not scanning, then a change happens if scanning was enabled.
101
+ if (this._scanState === 'starting' || this._scanState === 'started') {
102
+ if (!enable) {
103
+ this.emit('scanStop');
104
+ } else if (this._scanFilterDuplicates !== filterDuplicates) {
105
+ this._scanFilterDuplicates = filterDuplicates;
106
+
107
+ this.emit('scanStart', this._scanFilterDuplicates);
108
+ }
109
+ } else if (
110
+ (this._scanState === 'stopping' || this._scanState === 'stopped') &&
111
+ enable
112
+ ) {
113
+ // Someone started scanning on us.
114
+ this.emit('scanStart', this._scanFilterDuplicates);
115
+ }
116
+ };
117
+
118
+ Gap.prototype.onHciLeAdvertisingReport = function (
119
+ status,
120
+ type,
121
+ address,
122
+ addressType,
123
+ eir,
124
+ rssi
125
+ ) {
126
+ const previouslyDiscovered = !!this._discoveries[address];
127
+
128
+ let discoveryCount = previouslyDiscovered
129
+ ? this._discoveries[address].count
130
+ : 0;
131
+ let hasScanResponse = previouslyDiscovered
132
+ ? this._discoveries[address].hasScanResponse
133
+ : false;
134
+
135
+ if (type === LE_META_EVENT_TYPE_SCAN_RESPONSE) {
136
+ hasScanResponse = true;
137
+ }
138
+
139
+ discoveryCount++;
140
+
141
+ const advertisement = this.parseServices(
142
+ address,
143
+ eir,
144
+ previouslyDiscovered,
145
+ hasScanResponse
146
+ );
147
+
148
+ debug(`advertisement = ${JSON.stringify(advertisement, null, 0)}`);
149
+
150
+ const connectable =
151
+ type === LE_META_EVENT_TYPE_SCAN_RESPONSE && previouslyDiscovered
152
+ ? this._discoveries[address].connectable
153
+ : type !== LE_META_EVENT_TYPE_CONNECTABLE;
154
+ const scannable = type === LE_META_EVENT_TYPE_SCANNABLE;
155
+
156
+ this._discoveries[address] = {
157
+ address: address,
158
+ addressType: addressType,
159
+ connectable: connectable,
160
+ advertisement: advertisement,
161
+ rssi: rssi,
162
+ count: discoveryCount,
163
+ hasScanResponse: hasScanResponse
164
+ };
165
+
166
+ // only report after a scan response event or if non-connectable or more than one discovery without a scan response, so more data can be collected
167
+ if (
168
+ type === LE_META_EVENT_TYPE_SCAN_RESPONSE ||
169
+ !connectable ||
170
+ (discoveryCount > 1 && !hasScanResponse) ||
171
+ process.env.NOBLE_REPORT_ALL_HCI_EVENTS
172
+ ) {
173
+ this.emit(
174
+ 'discover',
175
+ status,
176
+ address,
177
+ addressType,
178
+ connectable,
179
+ advertisement,
180
+ rssi,
181
+ scannable
182
+ );
183
+ }
184
+ };
185
+
186
+ Gap.prototype.onHciLeExtendedAdvertisingReport = function (
187
+ status,
188
+ type,
189
+ address,
190
+ addressType,
191
+ txpower,
192
+ rssi,
193
+ eir
194
+ ) {
195
+ const previouslyDiscovered = !!this._discoveries[address];
196
+
197
+ let discoveryCount = previouslyDiscovered
198
+ ? this._discoveries[address].count
199
+ : 0;
200
+ let hasScanResponse = previouslyDiscovered
201
+ ? this._discoveries[address].hasScanResponse
202
+ : false;
203
+
204
+ if (type & LE_META_EXTENDED_EVENT_TYPE_SCAN_RESPONSE_MASK) {
205
+ hasScanResponse = true;
206
+ }
207
+
208
+ discoveryCount++;
209
+
210
+ const advertisement = this.parseServices(
211
+ address,
212
+ eir,
213
+ previouslyDiscovered,
214
+ hasScanResponse,
215
+ txpower
216
+ );
217
+
218
+ debug(`advertisement = ${JSON.stringify(advertisement, null, 0)}`);
219
+
220
+ const connectable =
221
+ type & 0x8 && previouslyDiscovered
222
+ ? this._discoveries[address].connectable
223
+ : type & LE_META_EXTENDED_EVENT_TYPE_CONNECTABLE_MASK;
224
+ const scannable = type & LE_META_EXTENDED_EVENT_TYPE_SCANNABLE_MASK ? 1 : 0;
225
+ const incomplete = type & LE_META_EXTENDED_EVENT_TYPE_INCOMPLETE_MASK ? 1 : 0;
226
+
227
+ this._discoveries[address] = {
228
+ address: address,
229
+ addressType: addressType,
230
+ connectable: connectable,
231
+ advertisement: advertisement,
232
+ rssi: rssi,
233
+ count: discoveryCount,
234
+ hasScanResponse: hasScanResponse
235
+ };
236
+
237
+ // only report after a scan response event or if non-connectable or more than one discovery without a scan response, so more data can be collected
238
+ if (
239
+ type & LE_META_EXTENDED_EVENT_TYPE_SCAN_RESPONSE_MASK ||
240
+ (!connectable && !incomplete) ||
241
+ (discoveryCount > 1 && !hasScanResponse) ||
242
+ process.env.NOBLE_REPORT_ALL_HCI_EVENTS
243
+ ) {
244
+ this.emit(
245
+ 'discover',
246
+ status,
247
+ address,
248
+ addressType,
249
+ connectable,
250
+ advertisement,
251
+ rssi,
252
+ scannable
253
+ );
254
+ }
255
+ };
256
+
257
+ Gap.prototype.parseServices = function (
258
+ address,
259
+ eir,
260
+ previouslyDiscovered,
261
+ hasScanResponse,
262
+ txpower
263
+ ) {
264
+ let i = 0;
265
+ const advertisement = previouslyDiscovered
266
+ ? this._discoveries[address].advertisement
267
+ : {
268
+ localName: undefined,
269
+ txPowerLevel: txpower,
270
+ manufacturerData: undefined,
271
+ serviceData: [],
272
+ serviceUuids: [],
273
+ solicitationServiceUuids: []
274
+ };
275
+
276
+ if (!hasScanResponse) {
277
+ // reset service data every non-scan response event
278
+ advertisement.serviceData = [];
279
+ advertisement.serviceUuids = [];
280
+ advertisement.serviceSolicitationUuids = [];
281
+ }
282
+
283
+ while (i + 1 < eir.length) {
284
+ const length = eir.readUInt8(i);
285
+
286
+ if (length < 1) {
287
+ debug(`invalid EIR data, length = ${length}`);
288
+ break;
289
+ }
290
+
291
+ const eirType = eir.readUInt8(i + 1); // https://www.bluetooth.org/en-us/specification/assigned-numbers/generic-access-profile
292
+
293
+ if (i + length + 1 > eir.length) {
294
+ debug('invalid EIR data, out of range of buffer length');
295
+ break;
296
+ }
297
+
298
+ const bytes = eir.slice(i + 2).slice(0, length - 1);
299
+
300
+ switch (eirType) {
301
+ case 0x02: // Incomplete List of 16-bit Service Class UUID
302
+ case 0x03: // Complete List of 16-bit Service Class UUIDs
303
+ for (let j = 0; j < bytes.length - 1; j += 2) {
304
+ const serviceUuid = bytes.readUInt16LE(j).toString(16);
305
+ if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) {
306
+ advertisement.serviceUuids.push(serviceUuid);
307
+ }
308
+ }
309
+ break;
310
+
311
+ case 0x06: // Incomplete List of 128-bit Service Class UUIDs
312
+ case 0x07: // Complete List of 128-bit Service Class UUIDs
313
+ for (let j = 0; j < bytes.length - 15; j += 16) {
314
+ const serviceUuid = bytes
315
+ .slice(j, j + 16)
316
+ .toString('hex')
317
+ .match(/.{1,2}/g)
318
+ .reverse()
319
+ .join('');
320
+ if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) {
321
+ advertisement.serviceUuids.push(serviceUuid);
322
+ }
323
+ }
324
+ break;
325
+
326
+ case 0x08: // Shortened Local Name
327
+ case 0x09: // Complete Local Name»
328
+ advertisement.localName = bytes.toString('utf8');
329
+ break;
330
+
331
+ case 0x0a: // Tx Power Level
332
+ advertisement.txPowerLevel = bytes.readInt8(0);
333
+ break;
334
+
335
+ case 0x14: // List of 16 bit solicitation UUIDs
336
+ for (let j = 0; j < bytes.length - 1; j += 2) {
337
+ const serviceSolicitationUuid = bytes.readUInt16LE(j).toString(16);
338
+ if (
339
+ advertisement.serviceSolicitationUuids.indexOf(
340
+ serviceSolicitationUuid
341
+ ) === -1
342
+ ) {
343
+ advertisement.serviceSolicitationUuids.push(
344
+ serviceSolicitationUuid
345
+ );
346
+ }
347
+ }
348
+ break;
349
+
350
+ case 0x15: // List of 128 bit solicitation UUIDs
351
+ for (let j = 0; j < bytes.length - 15; j += 16) {
352
+ const serviceSolicitationUuid = bytes
353
+ .slice(j, j + 16)
354
+ .toString('hex')
355
+ .match(/.{1,2}/g)
356
+ .reverse()
357
+ .join('');
358
+ if (
359
+ advertisement.serviceSolicitationUuids.indexOf(
360
+ serviceSolicitationUuid
361
+ ) === -1
362
+ ) {
363
+ advertisement.serviceSolicitationUuids.push(
364
+ serviceSolicitationUuid
365
+ );
366
+ }
367
+ }
368
+ break;
369
+
370
+ case 0x16: // 16-bit Service Data, there can be multiple occurences
371
+ advertisement.serviceData.push({
372
+ uuid: bytes
373
+ .slice(0, 2)
374
+ .toString('hex')
375
+ .match(/.{1,2}/g)
376
+ .reverse()
377
+ .join(''),
378
+ data: bytes.slice(2, bytes.length)
379
+ });
380
+ break;
381
+
382
+ case 0x20: // 32-bit Service Data, there can be multiple occurences
383
+ advertisement.serviceData.push({
384
+ uuid: bytes
385
+ .slice(0, 4)
386
+ .toString('hex')
387
+ .match(/.{1,2}/g)
388
+ .reverse()
389
+ .join(''),
390
+ data: bytes.slice(4, bytes.length)
391
+ });
392
+ break;
393
+
394
+ case 0x21: // 128-bit Service Data, there can be multiple occurences
395
+ advertisement.serviceData.push({
396
+ uuid: bytes
397
+ .slice(0, 16)
398
+ .toString('hex')
399
+ .match(/.{1,2}/g)
400
+ .reverse()
401
+ .join(''),
402
+ data: bytes.slice(16, bytes.length)
403
+ });
404
+ break;
405
+
406
+ case 0x1f: // List of 32 bit solicitation UUIDs
407
+ for (let j = 0; j < bytes.length - 3; j += 4) {
408
+ const serviceSolicitationUuid = bytes.readUInt32LE(j).toString(16);
409
+ if (
410
+ advertisement.serviceSolicitationUuids.indexOf(
411
+ serviceSolicitationUuid
412
+ ) === -1
413
+ ) {
414
+ advertisement.serviceSolicitationUuids.push(
415
+ serviceSolicitationUuid
416
+ );
417
+ }
418
+ }
419
+ break;
420
+
421
+ case 0xff: // Manufacturer Specific Data
422
+ advertisement.manufacturerData = bytes;
423
+ break;
424
+ }
425
+
426
+ i += length + 1;
427
+ }
428
+
429
+ return advertisement;
430
+ };
431
+
432
+ module.exports = Gap;