@comapeo/map-server 1.0.0-pre.2 → 1.0.0-pre.3
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/README.md +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/lib/download-request.d.ts.map +1 -1
- package/dist/lib/download-request.js +2 -3
- package/dist/lib/map-share.d.ts +1 -1
- package/dist/lib/map-share.d.ts.map +1 -1
- package/dist/lib/map-share.js +1 -1
- package/dist/lib/secret-stream-fetch.d.ts +1 -1
- package/dist/lib/secret-stream-fetch.d.ts.map +1 -1
- package/dist/lib/secret-stream-fetch.js +2 -1
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +6 -2
- package/dist/routes/downloads.d.ts +1 -1
- package/dist/routes/map-shares.d.ts +1 -1
- package/dist/routes/map-shares.d.ts.map +1 -1
- package/dist/routes/map-shares.js +7 -2
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/node_modules/@whatwg-node/server/cjs/index.js +0 -3
- package/node_modules/@whatwg-node/server/esm/index.js +0 -2
- package/package.json +2 -3
- package/src/index.ts +1 -2
- package/src/lib/download-request.ts +3 -4
- package/src/lib/map-share.ts +2 -2
- package/src/lib/secret-stream-fetch.ts +3 -2
- package/src/lib/utils.ts +9 -2
- package/src/routes/map-shares.ts +12 -3
- package/src/types.ts +7 -5
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ Content-Type: application/json
|
|
|
264
264
|
|
|
265
265
|
{
|
|
266
266
|
"mapId": "custom",
|
|
267
|
-
"receiverDeviceId": "
|
|
267
|
+
"receiverDeviceId": "a1b2c3d4..." // hex-encoded public key
|
|
268
268
|
}
|
|
269
269
|
```
|
|
270
270
|
|
|
@@ -326,7 +326,7 @@ POST /downloads
|
|
|
326
326
|
Content-Type: application/json
|
|
327
327
|
|
|
328
328
|
{
|
|
329
|
-
"senderDeviceId": "
|
|
329
|
+
"senderDeviceId": "a1b2c3d4e5f6...",
|
|
330
330
|
"shareId": "abc123...",
|
|
331
331
|
"mapShareUrls": ["http://192.168.1.100:9090/mapShares/abc123..."],
|
|
332
332
|
"estimatedSizeBytes": 12345678
|
|
@@ -370,7 +370,7 @@ POST /mapShares/{shareId}/decline
|
|
|
370
370
|
Content-Type: application/json
|
|
371
371
|
|
|
372
372
|
{
|
|
373
|
-
"senderDeviceId": "
|
|
373
|
+
"senderDeviceId": "a1b2c3d4e5f6...",
|
|
374
374
|
"mapShareUrls": ["http://192.168.1.100:9090/mapShares/abc123..."],
|
|
375
375
|
"reason": "disk_full" | "user_rejected" | "other reason"
|
|
376
376
|
}
|
|
@@ -385,7 +385,6 @@ Called on the receiver's local server. The server handles making the P2P request
|
|
|
385
385
|
```javascript
|
|
386
386
|
import { createServer } from '@comapeo/map-server'
|
|
387
387
|
import Hypercore from 'hypercore'
|
|
388
|
-
import z32 from 'z32'
|
|
389
388
|
|
|
390
389
|
const deviceAKeyPair = Hypercore.keyPair()
|
|
391
390
|
const serverA = createServer({
|
|
@@ -398,7 +397,7 @@ const serverA = createServer({
|
|
|
398
397
|
const { localPort } = await serverA.listen()
|
|
399
398
|
|
|
400
399
|
// Device B's public key (exchanged via your discovery mechanism)
|
|
401
|
-
const deviceBId = '
|
|
400
|
+
const deviceBId = 'a1b2c3d4e5f6...' // hex-encoded
|
|
402
401
|
|
|
403
402
|
// Create share
|
|
404
403
|
const res = await fetch(`http://127.0.0.1:${localPort}/mapShares`, {
|
|
@@ -559,7 +558,7 @@ All error responses follow this format:
|
|
|
559
558
|
| `DOWNLOAD_SHARE_DECLINED` | 409 | Cannot download a share that was declined |
|
|
560
559
|
| `DOWNLOAD_SHARE_NOT_PENDING` | 409 | Cannot download a share that is not pending |
|
|
561
560
|
| `ABORT_NOT_DOWNLOADING` | 409 | Cannot abort a download that is not in progress |
|
|
562
|
-
| `INVALID_SENDER_DEVICE_ID` | 400 | The sender device ID is not a valid
|
|
561
|
+
| `INVALID_SENDER_DEVICE_ID` | 400 | The sender device ID is not a valid hex-encoded public key |
|
|
563
562
|
|
|
564
563
|
### Generic Errors
|
|
565
564
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,YAAY,EACX,OAAO,EACP,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACnB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,YAAY,EACX,oBAAoB,EACpB,qBAAqB,GACrB,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAEjE,MAAM,MAAM,aAAa,GAAG;IAC3B,qBAAqB,EAAE,MAAM,GAAG,GAAG,CAAA;IACnC,aAAa,EAAE,MAAM,GAAG,GAAG,CAAA;IAC3B,eAAe,EAAE,MAAM,GAAG,GAAG,CAAA;IAC7B,OAAO,CAAC,EAAE;QACT,SAAS,EAAE,UAAU,CAAA;QACrB,SAAS,EAAE,UAAU,CAAA;KACrB,CAAA;CACD,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAOD,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa;kBA+C9B,aAAa;;;;;EA6BjC"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import http from 'node:http';
|
|
|
4
4
|
import { createServerAdapter } from '@whatwg-node/server';
|
|
5
5
|
import pDefer from 'p-defer';
|
|
6
6
|
import { Agent, createServer as createSecretStreamServer, } from 'secret-stream-http';
|
|
7
|
-
import z32 from 'z32';
|
|
8
7
|
import { Context } from './context.js';
|
|
9
8
|
import { fetchAPI } from './lib/fetch-api.js';
|
|
10
9
|
import { RootRouter } from './routes/root.js';
|
|
@@ -35,7 +34,7 @@ export function createServer(options) {
|
|
|
35
34
|
serverAdapter(req, res, {
|
|
36
35
|
isLocalhost: false,
|
|
37
36
|
// @ts-expect-error - the types for this are too hard and making them work would not add any type safety.
|
|
38
|
-
remoteDeviceId:
|
|
37
|
+
remoteDeviceId: Buffer.from(req.socket.remotePublicKey).toString('hex'),
|
|
39
38
|
});
|
|
40
39
|
});
|
|
41
40
|
const secretStreamServer = createSecretStreamServer(remoteHttpServer, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download-request.d.ts","sourceRoot":"","sources":["../../src/lib/download-request.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"download-request.d.ts","sourceRoot":"","sources":["../../src/lib/download-request.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAClE,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAItD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAG1D,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAC9C,IAAI,CAAC,oBAAoB,EAAE,cAAc,CAAC,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpE,qBAAa,eAAgB,SAAQ,gBAAgB,CACpD,YAAY,CAAC,OAAO,gBAAgB,CAAC,mBAAmB,CAAC,CAAC,CAC1D;;gBAkBC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,EAAE,YAAY,EAAE,GAAG,IAAI,EAAE,EAAE,oBAAoB,EAC/C,OAAO,EAAE;QAAE,SAAS,EAAE,UAAU,CAAC;QAAC,SAAS,EAAE,UAAU,CAAA;KAAE;IA4F1D,IAAI,KAAK,kBAER;IAED,MAAM;CAQN"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Agent as SecretStreamAgent } from 'secret-stream-http';
|
|
2
|
-
import z32 from 'z32';
|
|
3
2
|
import { TypedEventTarget } from '../lib/event-target.js';
|
|
4
3
|
import { StatusError } from './errors.js';
|
|
5
4
|
import { errors, jsonError } from './errors.js';
|
|
@@ -32,7 +31,7 @@ export class DownloadRequest extends TypedEventTarget {
|
|
|
32
31
|
};
|
|
33
32
|
let remotePublicKey;
|
|
34
33
|
try {
|
|
35
|
-
remotePublicKey =
|
|
34
|
+
remotePublicKey = Buffer.from(this.#state.senderDeviceId, 'hex');
|
|
36
35
|
}
|
|
37
36
|
catch {
|
|
38
37
|
throw new errors.INVALID_SENDER_DEVICE_ID(`Invalid sender device ID: ${this.#state.senderDeviceId}`);
|
|
@@ -81,7 +80,7 @@ export class DownloadRequest extends TypedEventTarget {
|
|
|
81
80
|
});
|
|
82
81
|
}
|
|
83
82
|
async #start({ mapShareUrls, stream, }) {
|
|
84
|
-
const downloadUrls = mapShareUrls.map((baseUrl) => new URL('download', addTrailingSlash(baseUrl)));
|
|
83
|
+
const downloadUrls = mapShareUrls.map((baseUrl) => new URL('download', addTrailingSlash(baseUrl))); // grrrr TS
|
|
85
84
|
const response = await secretStreamFetch(downloadUrls, {
|
|
86
85
|
dispatcher: this.#dispatcher,
|
|
87
86
|
});
|
package/dist/lib/map-share.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type MapShareOptions = MapInfo & {
|
|
|
7
7
|
* are supported because the server might have multiple network interfaces
|
|
8
8
|
* with different IP addresses
|
|
9
9
|
*/
|
|
10
|
-
baseUrls: string[];
|
|
10
|
+
baseUrls: readonly [string, ...string[]];
|
|
11
11
|
/** The device ID of the receiver */
|
|
12
12
|
receiverDeviceId: string;
|
|
13
13
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-share.d.ts","sourceRoot":"","sources":["../../src/lib/map-share.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EACN,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,OAAO,EACZ,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAG1D,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG;IACvC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"map-share.d.ts","sourceRoot":"","sources":["../../src/lib/map-share.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EACN,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,OAAO,EACZ,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAG1D,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG;IACvC;;;;OAIG;IACH,QAAQ,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAA;IACxC,oCAAoC;IACpC,gBAAgB,EAAE,MAAM,CAAA;CACxB,CAAA;AAED;;GAEG;AACH,qBAAa,QAAS,SAAQ,gBAAgB,CAC7C,YAAY,CAAC,OAAO,gBAAgB,CAAC,CACrC;;gBAGY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,EAAE,EAAE,eAAe;IAevE,IAAI,OAAO,WAEV;IAED,IAAI,KAAK,kBAER;IAED;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,cAAc,GAAG,QAAQ;IAkBpD;;OAEG;IACH,OAAO,CACN,MAAM,EAAE,OAAO,CAAC,mBAAmB,EAAE;QAAE,MAAM,EAAE,UAAU,CAAA;KAAE,CAAC,CAAC,QAAQ,CAAC;IAUvE;;OAEG;IACH,MAAM;CAiBN;AAED;;;;;;;GAOG;AACH,qBAAa,gBAAiB,SAAQ,gBAAgB,CACrD,YAAY,CAAC,OAAO,gBAAgB,CAAC,mBAAmB,CAAC,CAAC,CAC1D;;gBAOY,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC;IAyChD,IAAI,QAAQ,aAEX;IAED,IAAI,KAAK,wBAER;IAED,MAAM;CAQN"}
|
package/dist/lib/map-share.js
CHANGED
|
@@ -14,7 +14,7 @@ export class MapShare extends TypedEventTarget {
|
|
|
14
14
|
this.#state = {
|
|
15
15
|
...mapInfo,
|
|
16
16
|
shareId,
|
|
17
|
-
mapShareUrls: baseUrls.map((baseUrl) => new URL(`${shareId}`, addTrailingSlash(baseUrl)).href),
|
|
17
|
+
mapShareUrls: baseUrls.map((baseUrl) => new URL(`${shareId}`, addTrailingSlash(baseUrl)).href), // grrrr TS
|
|
18
18
|
receiverDeviceId,
|
|
19
19
|
mapShareCreatedAt: Date.now(),
|
|
20
20
|
status: 'pending',
|
|
@@ -3,5 +3,5 @@ import { fetch as secretStreamFetchOrig } from 'secret-stream-http';
|
|
|
3
3
|
* A wrapper around secret-stream-http's fetch that tries multiple URLs until one works.
|
|
4
4
|
* This is useful when the server has multiple IPs for different network interfaces.
|
|
5
5
|
*/
|
|
6
|
-
export declare function secretStreamFetch(urls: string | URL | Array<string | URL
|
|
6
|
+
export declare function secretStreamFetch(urls: string | URL | readonly [string | URL, ...Array<string | URL>], options: Parameters<typeof secretStreamFetchOrig>[1]): Promise<Response>;
|
|
7
7
|
//# sourceMappingURL=secret-stream-fetch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-stream-fetch.d.ts","sourceRoot":"","sources":["../../src/lib/secret-stream-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"secret-stream-fetch.d.ts","sourceRoot":"","sources":["../../src/lib/secret-stream-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAKnE;;;GAGG;AACH,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC,MAAM,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,EACpE,OAAO,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,qBA+BpD"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { fetch as secretStreamFetchOrig } from 'secret-stream-http';
|
|
2
2
|
import { errors } from './errors.js';
|
|
3
|
+
import { isArrayReadonly } from './utils.js';
|
|
3
4
|
/**
|
|
4
5
|
* A wrapper around secret-stream-http's fetch that tries multiple URLs until one works.
|
|
5
6
|
* This is useful when the server has multiple IPs for different network interfaces.
|
|
6
7
|
*/
|
|
7
8
|
export async function secretStreamFetch(urls, options) {
|
|
8
|
-
if (!
|
|
9
|
+
if (!isArrayReadonly(urls)) {
|
|
9
10
|
urls = [urls];
|
|
10
11
|
}
|
|
11
12
|
let response;
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -29,4 +29,5 @@ export declare function getStyleBbox(style: SMPStyle): BBox;
|
|
|
29
29
|
export declare function getStyleMaxZoom(style: SMPStyle): number;
|
|
30
30
|
export declare function getStyleMinZoom(style: SMPStyle): number;
|
|
31
31
|
export declare function addTrailingSlash(url: string): string;
|
|
32
|
+
export declare function isArrayReadonly<T, U>(value: U | readonly T[]): value is readonly T[];
|
|
32
33
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/lib/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAElD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,OAAO,sBAS/C;AAED,wBAAgB,IAAI,SAAK;AAEzB,wBAAgB,UAAU,WAEzB;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAMrE;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAO7D;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,IAAI,CAUzD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAUlD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAOvD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAOvD;AAMD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAID,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EACnC,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC,EAAE,GACrB,KAAK,IAAI,SAAS,CAAC,EAAE,CAEvB"}
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { randomBytes } from 'crypto';
|
|
3
|
-
import z32 from 'z32';
|
|
4
3
|
/**
|
|
5
4
|
* If the argument is an `Error` instance, return its `code` property if it is a string.
|
|
6
5
|
* Otherwise, returns `undefined`.
|
|
@@ -24,7 +23,7 @@ export function getErrorCode(maybeError) {
|
|
|
24
23
|
}
|
|
25
24
|
export function noop() { }
|
|
26
25
|
export function generateId() {
|
|
27
|
-
return
|
|
26
|
+
return randomBytes(8).toString('hex');
|
|
28
27
|
}
|
|
29
28
|
export function getOrInsert(map, key, value) {
|
|
30
29
|
if (map.has(key)) {
|
|
@@ -94,3 +93,8 @@ function isNonEmptyArray(arr) {
|
|
|
94
93
|
export function addTrailingSlash(url) {
|
|
95
94
|
return url.endsWith('/') ? url : url + '/';
|
|
96
95
|
}
|
|
96
|
+
// Typescript's Array.isArray definition does not work as a type guard for
|
|
97
|
+
// readonly arrays, so we need to define our own type guard for that
|
|
98
|
+
export function isArrayReadonly(value) {
|
|
99
|
+
return Array.isArray(value);
|
|
100
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { Context } from '../context.js';
|
|
|
3
3
|
import { type RouterExternal } from '../types.js';
|
|
4
4
|
declare const DownloadCreateRequest: T.TObject<{
|
|
5
5
|
senderDeviceId: T.TString;
|
|
6
|
-
mapShareUrls: T.
|
|
6
|
+
mapShareUrls: T.TUnsafe<readonly [string, ...string[]]>;
|
|
7
7
|
shareId: T.TString;
|
|
8
8
|
estimatedSizeBytes: T.TNumber;
|
|
9
9
|
}>;
|
|
@@ -8,7 +8,7 @@ declare const MapShareCreateRequest: T.TObject<{
|
|
|
8
8
|
export type MapShareCreateParams = Static<typeof MapShareCreateRequest>;
|
|
9
9
|
declare const LocalMapShareDeclineRequest: T.TObject<{
|
|
10
10
|
reason: T.TUnion<[T.TLiteral<"disk_full">, T.TLiteral<"user_rejected">, T.TString]>;
|
|
11
|
-
mapShareUrls: T.
|
|
11
|
+
mapShareUrls: T.TUnsafe<readonly [string, ...string[]]>;
|
|
12
12
|
senderDeviceId: T.TString;
|
|
13
13
|
}>;
|
|
14
14
|
export type MapShareDeclineParams = Static<typeof LocalMapShareDeclineRequest>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-shares.d.ts","sourceRoot":"","sources":["../../src/routes/map-shares.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"map-shares.d.ts","sourceRoot":"","sources":["../../src/routes/map-shares.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;AAGhD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAQ5C,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,aAAa,CAAA;AAEpB,QAAA,MAAM,qBAAqB;;;EAGzB,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEvE,QAAA,MAAM,2BAA2B;;;;EAO/B,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAW9E,wBAAgB,eAAe,CAC9B,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1B,GAAG,EAAE,OAAO,GACV,cAAc,CAuKhB"}
|
|
@@ -3,7 +3,6 @@ import { IttyRouter } from 'itty-router';
|
|
|
3
3
|
import { fetch as secretStreamFetch, Agent as SecretStreamAgent, } from 'secret-stream-http';
|
|
4
4
|
import { Type as T } from 'typebox';
|
|
5
5
|
import { Compile } from 'typebox/compile';
|
|
6
|
-
import z32 from 'z32';
|
|
7
6
|
import { errors, StatusError } from '../lib/errors.js';
|
|
8
7
|
import { createEventStreamResponse } from '../lib/event-stream-response.js';
|
|
9
8
|
import { MapShare } from '../lib/map-share.js';
|
|
@@ -100,7 +99,7 @@ export function MapSharesRouter({ base }, ctx) {
|
|
|
100
99
|
throw new errors.INVALID_REQUEST();
|
|
101
100
|
}
|
|
102
101
|
const { senderDeviceId, mapShareUrls, reason } = parsedBody;
|
|
103
|
-
const remotePublicKey =
|
|
102
|
+
const remotePublicKey = Buffer.from(senderDeviceId, 'hex');
|
|
104
103
|
const keyPair = ctx.getKeyPair();
|
|
105
104
|
let response;
|
|
106
105
|
// The sharer could have multiple IPs for different network interfaces, and
|
|
@@ -188,5 +187,11 @@ function getRemoteBaseUrls(requestUrl, remotePort) {
|
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
189
|
}
|
|
190
|
+
if (!arrayAtLeastOne(baseUrls)) {
|
|
191
|
+
throw new Error('No non-internal IPv4 addresses found');
|
|
192
|
+
}
|
|
191
193
|
return baseUrls;
|
|
192
194
|
}
|
|
195
|
+
function arrayAtLeastOne(arr) {
|
|
196
|
+
return arr.length >= 1;
|
|
197
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type MapShareStatus = Static<typeof MapShareStateUpdate>['status'];
|
|
|
27
27
|
export type DownloadStateUpdate = Extract<MapShareStateUpdate, {
|
|
28
28
|
status: 'downloading' | 'completed' | 'error' | 'canceled' | 'aborted';
|
|
29
29
|
}>;
|
|
30
|
-
export declare const MapShareUrls: T.
|
|
30
|
+
export declare const MapShareUrls: T.TUnsafe<readonly [string, ...string[]]>;
|
|
31
31
|
export declare const ShareId: T.TString;
|
|
32
32
|
export type ShareId = Static<typeof ShareId>;
|
|
33
33
|
export declare const EstimatedSizeBytes: T.TNumber;
|
|
@@ -43,7 +43,7 @@ declare const MapInfo: T.TObject<{
|
|
|
43
43
|
declare const MapShareBase: T.TIntersect<[T.TObject<{
|
|
44
44
|
receiverDeviceId: T.TString;
|
|
45
45
|
shareId: T.TString;
|
|
46
|
-
mapShareUrls: T.
|
|
46
|
+
mapShareUrls: T.TUnsafe<readonly [string, ...string[]]>;
|
|
47
47
|
mapShareCreatedAt: T.TNumber;
|
|
48
48
|
}>, T.TObject<{
|
|
49
49
|
mapId: T.TString;
|
|
@@ -57,7 +57,7 @@ declare const MapShareBase: T.TIntersect<[T.TObject<{
|
|
|
57
57
|
export declare const MapShareState: T.TIntersect<[T.TIntersect<[T.TObject<{
|
|
58
58
|
receiverDeviceId: T.TString;
|
|
59
59
|
shareId: T.TString;
|
|
60
|
-
mapShareUrls: T.
|
|
60
|
+
mapShareUrls: T.TUnsafe<readonly [string, ...string[]]>;
|
|
61
61
|
mapShareCreatedAt: T.TNumber;
|
|
62
62
|
}>, T.TObject<{
|
|
63
63
|
mapId: T.TString;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;AAEhD,eAAO,MAAM,qBAAqB,6EAYhC,CAAA;AAEF,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;IAgDvB,CAAA;AAEF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAA;AACpE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC,QAAQ,CAAC,CAAA;AAEzE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACxC,mBAAmB,EACnB;IAAE,MAAM,EAAE,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;CAAE,CAC1E,CAAA;AAED,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;AAEhD,eAAO,MAAM,qBAAqB,6EAYhC,CAAA;AAEF,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;IAgDvB,CAAA;AAEF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAA;AACpE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC,QAAQ,CAAC,CAAA;AAEzE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACxC,mBAAmB,EACnB;IAAE,MAAM,EAAE,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;CAAE,CAC1E,CAAA;AAED,eAAO,MAAM,YAAY,2CAMxB,CAAA;AACD,eAAO,MAAM,OAAO,WAGlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,OAAO,CAAC,CAAA;AAC5C,eAAO,MAAM,kBAAkB,WAE7B,CAAA;AAEF,QAAA,MAAM,OAAO;;;;;;;;EAcX,CAAA;AAEF,QAAA,MAAM,YAAY;;;;;;;;;;;;;IAYhB,CAAA;AAEF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAAmD,CAAA;AAE7E,MAAM,MAAM,aAAa,GAAG,wBAAwB,CACnD,MAAM,CAAC,OAAO,YAAY,CAAC,EAC3B,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAClC,CAAA;AAED,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,OAAO,CAAC,CAAA;AAE5C,MAAM,MAAM,YAAY,GAAG;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,wBAAwB,CAAC,IAAI,EAAE,KAAK,IAAI,KAAK,SAAS,OAAO,GACtE,IAAI,GAAG,KAAK,GACZ,KAAK,CAAA;AAER,MAAM,MAAM,kBAAkB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,GACxE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GACb;KAAG,CAAC,IAAI,CAAC,GAAG,CAAC;CAAE,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5B,KAAK,GACN,KAAK,CAAA;AAER;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;CACpE,CAAA;AAED,MAAM,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA"}
|
package/dist/types.js
CHANGED
|
@@ -56,10 +56,10 @@ const MapShareStateUpdate = T.Union([
|
|
|
56
56
|
}),
|
|
57
57
|
}),
|
|
58
58
|
]);
|
|
59
|
-
export const MapShareUrls = T.Array(T.String({ format: 'uri' }), {
|
|
59
|
+
export const MapShareUrls = T.Unsafe(T.Array(T.String({ format: 'uri' }), {
|
|
60
60
|
minItems: 1,
|
|
61
61
|
description: 'List of map share URLs (for each network interface of the sharer)',
|
|
62
|
-
});
|
|
62
|
+
}));
|
|
63
63
|
export const ShareId = T.String({
|
|
64
64
|
minLength: 1,
|
|
65
65
|
description: 'The ID of the map share',
|
|
@@ -7,11 +7,8 @@ tslib_1.__exportStar(require("./types.js"), exports);
|
|
|
7
7
|
tslib_1.__exportStar(require("./utils.js"), exports);
|
|
8
8
|
tslib_1.__exportStar(require("./plugins/types.js"), exports);
|
|
9
9
|
tslib_1.__exportStar(require("./plugins/useCors.js"), exports);
|
|
10
|
-
tslib_1.__exportStar(require("./plugins/useErrorHandling.js"), exports);
|
|
11
10
|
tslib_1.__exportStar(require("./plugins/useContentEncoding.js"), exports);
|
|
12
11
|
tslib_1.__exportStar(require("./uwebsockets.js"), exports);
|
|
13
|
-
var fetch_1 = require("@whatwg-node/fetch");
|
|
14
|
-
Object.defineProperty(exports, "Response", { enumerable: true, get: function () { return fetch_1.Response; } });
|
|
15
12
|
var disposablestack_1 = require("@whatwg-node/disposablestack");
|
|
16
13
|
Object.defineProperty(exports, "DisposableSymbols", { enumerable: true, get: function () { return disposablestack_1.DisposableSymbols; } });
|
|
17
14
|
tslib_1.__exportStar(require("@envelop/instrumentation"), exports);
|
|
@@ -3,9 +3,7 @@ export * from './types.js';
|
|
|
3
3
|
export * from './utils.js';
|
|
4
4
|
export * from './plugins/types.js';
|
|
5
5
|
export * from './plugins/useCors.js';
|
|
6
|
-
export * from './plugins/useErrorHandling.js';
|
|
7
6
|
export * from './plugins/useContentEncoding.js';
|
|
8
7
|
export * from './uwebsockets.js';
|
|
9
|
-
export { Response } from '@whatwg-node/fetch';
|
|
10
8
|
export { DisposableSymbols } from '@whatwg-node/disposablestack';
|
|
11
9
|
export * from '@envelop/instrumentation';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comapeo/map-server",
|
|
3
|
-
"version": "1.0.0-pre.
|
|
3
|
+
"version": "1.0.0-pre.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -73,8 +73,7 @@
|
|
|
73
73
|
"secret-stream-http": "^1.0.1",
|
|
74
74
|
"styled-map-package": "^4.0.1",
|
|
75
75
|
"typebox": "^1.0.61",
|
|
76
|
-
"typed-event-target": "^3.4.0"
|
|
77
|
-
"z32": "^1.1.0"
|
|
76
|
+
"typed-event-target": "^3.4.0"
|
|
78
77
|
},
|
|
79
78
|
"bundleDependencies": [
|
|
80
79
|
"@whatwg-node/server"
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
Agent,
|
|
10
10
|
createServer as createSecretStreamServer,
|
|
11
11
|
} from 'secret-stream-http'
|
|
12
|
-
import z32 from 'z32'
|
|
13
12
|
|
|
14
13
|
import { Context } from './context.js'
|
|
15
14
|
import { fetchAPI } from './lib/fetch-api.js'
|
|
@@ -79,7 +78,7 @@ export function createServer(options: ServerOptions) {
|
|
|
79
78
|
serverAdapter(req, res, {
|
|
80
79
|
isLocalhost: false,
|
|
81
80
|
// @ts-expect-error - the types for this are too hard and making them work would not add any type safety.
|
|
82
|
-
remoteDeviceId:
|
|
81
|
+
remoteDeviceId: Buffer.from(req.socket.remotePublicKey).toString('hex'),
|
|
83
82
|
})
|
|
84
83
|
})
|
|
85
84
|
const secretStreamServer = createSecretStreamServer(remoteHttpServer, {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Agent as SecretStreamAgent } from 'secret-stream-http'
|
|
2
|
-
import z32 from 'z32'
|
|
3
2
|
|
|
4
3
|
import { TypedEventTarget } from '../lib/event-target.js'
|
|
5
4
|
import type { DownloadCreateParams } from '../routes/downloads.js'
|
|
@@ -46,7 +45,7 @@ export class DownloadRequest extends TypedEventTarget<
|
|
|
46
45
|
}
|
|
47
46
|
let remotePublicKey: Uint8Array
|
|
48
47
|
try {
|
|
49
|
-
remotePublicKey =
|
|
48
|
+
remotePublicKey = Buffer.from(this.#state.senderDeviceId, 'hex')
|
|
50
49
|
} catch {
|
|
51
50
|
throw new errors.INVALID_SENDER_DEVICE_ID(
|
|
52
51
|
`Invalid sender device ID: ${this.#state.senderDeviceId}`,
|
|
@@ -100,14 +99,14 @@ export class DownloadRequest extends TypedEventTarget<
|
|
|
100
99
|
mapShareUrls,
|
|
101
100
|
stream,
|
|
102
101
|
}: {
|
|
103
|
-
mapShareUrls: string[]
|
|
102
|
+
mapShareUrls: readonly [string, ...string[]]
|
|
104
103
|
stream: WritableStream<Uint8Array>
|
|
105
104
|
remotePublicKey: Uint8Array
|
|
106
105
|
keyPair: { publicKey: Uint8Array; secretKey: Uint8Array }
|
|
107
106
|
}) {
|
|
108
107
|
const downloadUrls = mapShareUrls.map(
|
|
109
108
|
(baseUrl) => new URL('download', addTrailingSlash(baseUrl)),
|
|
110
|
-
)
|
|
109
|
+
) as unknown as [URL, ...URL[]] // grrrr TS
|
|
111
110
|
const response = await secretStreamFetch(downloadUrls, {
|
|
112
111
|
dispatcher: this.#dispatcher,
|
|
113
112
|
})
|
package/src/lib/map-share.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type MapShareOptions = MapInfo & {
|
|
|
15
15
|
* are supported because the server might have multiple network interfaces
|
|
16
16
|
* with different IP addresses
|
|
17
17
|
*/
|
|
18
|
-
baseUrls: string[]
|
|
18
|
+
baseUrls: readonly [string, ...string[]]
|
|
19
19
|
/** The device ID of the receiver */
|
|
20
20
|
receiverDeviceId: string
|
|
21
21
|
}
|
|
@@ -36,7 +36,7 @@ export class MapShare extends TypedEventTarget<
|
|
|
36
36
|
shareId,
|
|
37
37
|
mapShareUrls: baseUrls.map(
|
|
38
38
|
(baseUrl) => new URL(`${shareId}`, addTrailingSlash(baseUrl)).href,
|
|
39
|
-
),
|
|
39
|
+
) as unknown as readonly [string, ...string[]], // grrrr TS
|
|
40
40
|
receiverDeviceId,
|
|
41
41
|
mapShareCreatedAt: Date.now(),
|
|
42
42
|
status: 'pending',
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { fetch as secretStreamFetchOrig } from 'secret-stream-http'
|
|
2
2
|
|
|
3
3
|
import { errors } from './errors.js'
|
|
4
|
+
import { isArrayReadonly } from './utils.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* A wrapper around secret-stream-http's fetch that tries multiple URLs until one works.
|
|
7
8
|
* This is useful when the server has multiple IPs for different network interfaces.
|
|
8
9
|
*/
|
|
9
10
|
export async function secretStreamFetch(
|
|
10
|
-
urls: string | URL | Array<string | URL
|
|
11
|
+
urls: string | URL | readonly [string | URL, ...Array<string | URL>],
|
|
11
12
|
options: Parameters<typeof secretStreamFetchOrig>[1],
|
|
12
13
|
) {
|
|
13
|
-
if (!
|
|
14
|
+
if (!isArrayReadonly(urls)) {
|
|
14
15
|
urls = [urls]
|
|
15
16
|
}
|
|
16
17
|
let response: Response | undefined
|
package/src/lib/utils.ts
CHANGED
|
@@ -2,7 +2,6 @@ import crypto from 'node:crypto'
|
|
|
2
2
|
|
|
3
3
|
import { randomBytes } from 'crypto'
|
|
4
4
|
import type { SMPStyle } from 'styled-map-package'
|
|
5
|
-
import z32 from 'z32'
|
|
6
5
|
|
|
7
6
|
import type { BBox } from '../types.js'
|
|
8
7
|
|
|
@@ -33,7 +32,7 @@ export function getErrorCode(maybeError: unknown) {
|
|
|
33
32
|
export function noop() {}
|
|
34
33
|
|
|
35
34
|
export function generateId() {
|
|
36
|
-
return
|
|
35
|
+
return randomBytes(8).toString('hex')
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
|
@@ -108,3 +107,11 @@ function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
|
|
|
108
107
|
export function addTrailingSlash(url: string): string {
|
|
109
108
|
return url.endsWith('/') ? url : url + '/'
|
|
110
109
|
}
|
|
110
|
+
|
|
111
|
+
// Typescript's Array.isArray definition does not work as a type guard for
|
|
112
|
+
// readonly arrays, so we need to define our own type guard for that
|
|
113
|
+
export function isArrayReadonly<T, U>(
|
|
114
|
+
value: U | readonly T[],
|
|
115
|
+
): value is readonly T[] {
|
|
116
|
+
return Array.isArray(value)
|
|
117
|
+
}
|
package/src/routes/map-shares.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from 'secret-stream-http'
|
|
8
8
|
import { Type as T, type Static } from 'typebox'
|
|
9
9
|
import { Compile } from 'typebox/compile'
|
|
10
|
-
import z32 from 'z32'
|
|
11
10
|
|
|
12
11
|
import type { Context } from '../context.js'
|
|
13
12
|
import { errors, StatusError } from '../lib/errors.js'
|
|
@@ -151,7 +150,7 @@ export function MapSharesRouter(
|
|
|
151
150
|
throw new errors.INVALID_REQUEST()
|
|
152
151
|
}
|
|
153
152
|
const { senderDeviceId, mapShareUrls, reason } = parsedBody
|
|
154
|
-
const remotePublicKey =
|
|
153
|
+
const remotePublicKey = Buffer.from(senderDeviceId, 'hex')
|
|
155
154
|
const keyPair = ctx.getKeyPair()
|
|
156
155
|
let response: Response | undefined
|
|
157
156
|
// The sharer could have multiple IPs for different network interfaces, and
|
|
@@ -227,7 +226,10 @@ export function MapSharesRouter(
|
|
|
227
226
|
/**
|
|
228
227
|
* Get the base URLs for downloads for all non-internal IPv4 addresses of the machine
|
|
229
228
|
*/
|
|
230
|
-
function getRemoteBaseUrls(
|
|
229
|
+
function getRemoteBaseUrls(
|
|
230
|
+
requestUrl: string,
|
|
231
|
+
remotePort: number,
|
|
232
|
+
): readonly [string, ...string[]] {
|
|
231
233
|
requestUrl = addTrailingSlash(requestUrl)
|
|
232
234
|
const interfaces = os.networkInterfaces()
|
|
233
235
|
const baseUrls: string[] = []
|
|
@@ -242,5 +244,12 @@ function getRemoteBaseUrls(requestUrl: string, remotePort: number): string[] {
|
|
|
242
244
|
}
|
|
243
245
|
}
|
|
244
246
|
}
|
|
247
|
+
if (!arrayAtLeastOne(baseUrls)) {
|
|
248
|
+
throw new Error('No non-internal IPv4 addresses found')
|
|
249
|
+
}
|
|
245
250
|
return baseUrls
|
|
246
251
|
}
|
|
252
|
+
|
|
253
|
+
function arrayAtLeastOne<T>(arr: readonly T[]): arr is readonly [T, ...T[]] {
|
|
254
|
+
return arr.length >= 1
|
|
255
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -73,11 +73,13 @@ export type DownloadStateUpdate = Extract<
|
|
|
73
73
|
{ status: 'downloading' | 'completed' | 'error' | 'canceled' | 'aborted' }
|
|
74
74
|
>
|
|
75
75
|
|
|
76
|
-
export const MapShareUrls = T.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
export const MapShareUrls = T.Unsafe<readonly [string, ...string[]]>(
|
|
77
|
+
T.Array(T.String({ format: 'uri' }), {
|
|
78
|
+
minItems: 1,
|
|
79
|
+
description:
|
|
80
|
+
'List of map share URLs (for each network interface of the sharer)',
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
81
83
|
export const ShareId = T.String({
|
|
82
84
|
minLength: 1,
|
|
83
85
|
description: 'The ID of the map share',
|