@comapeo/map-server 1.0.0-pre.2 → 1.0.0-pre.4

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 CHANGED
@@ -264,7 +264,7 @@ Content-Type: application/json
264
264
 
265
265
  {
266
266
  "mapId": "custom",
267
- "receiverDeviceId": "kmx8sejfn..." // z32-encoded public key
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": "z32-encoded-public-key",
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": "z32-encoded-public-key",
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 = 'kmx8sejfn...' // z32-encoded
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 z32-encoded public key |
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 CHANGED
@@ -1,4 +1,5 @@
1
1
  export { errors } from './lib/errors.js';
2
+ export { CUSTOM_MAP_ID, DEFAULT_MAP_ID } from './lib/constants.js';
2
3
  export type { MapInfo, MapShareState, MapShareStateUpdate, DownloadStateUpdate, } from './types.js';
3
4
  export type { DownloadState } from './lib/download-request.js';
4
5
  export type { MapShareCreateParams, MapShareDeclineParams, } from './routes/map-shares.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAElE,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;;;;;EA+BjC"}
package/dist/index.js CHANGED
@@ -4,17 +4,17 @@ 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';
11
10
  export { errors } from './lib/errors.js';
11
+ export { CUSTOM_MAP_ID, DEFAULT_MAP_ID } from './lib/constants.js';
12
12
  export function createServer(options) {
13
13
  validateOptions(options);
14
14
  if (!options.keyPair) {
15
15
  options.keyPair = Agent.keyPair();
16
16
  }
17
- const deferredListen = pDefer();
17
+ let deferredListen = pDefer();
18
18
  const context = new Context({
19
19
  ...options,
20
20
  keyPair: options.keyPair,
@@ -35,7 +35,7 @@ export function createServer(options) {
35
35
  serverAdapter(req, res, {
36
36
  isLocalhost: false,
37
37
  // @ts-expect-error - the types for this are too hard and making them work would not add any type safety.
38
- remoteDeviceId: z32.encode(req.socket.remotePublicKey),
38
+ remoteDeviceId: Buffer.from(req.socket.remotePublicKey).toString('hex'),
39
39
  });
40
40
  });
41
41
  const secretStreamServer = createSecretStreamServer(remoteHttpServer, {
@@ -79,6 +79,8 @@ export function createServer(options) {
79
79
  once(localHttpServer, 'close'),
80
80
  once(secretStreamServer, 'close'),
81
81
  ]);
82
+ // Reset deferred listen for potential restart with different ports
83
+ deferredListen = pDefer();
82
84
  },
83
85
  };
84
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"download-request.d.ts","sourceRoot":"","sources":["../../src/lib/download-request.ts"],"names":[],"mappings":"AAGA,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
+ {"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 = z32.decode(this.#state.senderDeviceId);
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}`);
@@ -52,7 +51,7 @@ export class DownloadRequest extends TypedEventTarget {
52
51
  }
53
52
  else if (getErrorCode(error)) {
54
53
  // Specific known error from the server
55
- this.#updateState({ status: 'error', error });
54
+ this.#updateState({ status: 'error', error: jsonError(error) });
56
55
  }
57
56
  else {
58
57
  // Once the download has started, the sender can only close the
@@ -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
  });
@@ -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;IAClB,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"}
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"}
@@ -1,5 +1,5 @@
1
1
  import { TypedEventTarget } from '../lib/event-target.js';
2
- import { errors } from './errors.js';
2
+ import { errors, jsonError } from './errors.js';
3
3
  import { StateUpdateEvent } from './state-update-event.js';
4
4
  import { addTrailingSlash, generateId, getErrorCode } from './utils.js';
5
5
  /**
@@ -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',
@@ -117,7 +117,7 @@ export class DownloadResponse extends TypedEventTarget {
117
117
  this.#updateState({ status: 'aborted' });
118
118
  }
119
119
  else {
120
- this.#updateState({ status: 'error', error });
120
+ this.#updateState({ status: 'error', error: jsonError(error) });
121
121
  }
122
122
  });
123
123
  this.#response = new Response(this.#stream.readable, {
@@ -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>, options: Parameters<typeof secretStreamFetchOrig>[1]): Promise<Response>;
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;AAInE;;;GAGG;AACH,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,EACxC,OAAO,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,qBA+BpD"}
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 (!Array.isArray(urls)) {
9
+ if (!isArrayReadonly(urls)) {
9
10
  urls = [urls];
10
11
  }
11
12
  let response;
@@ -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
@@ -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;AAGlD,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"}
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 z32.encode(randomBytes(8));
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.TArray<T.TString>;
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.TArray<T.TString>;
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;AAIhD,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"}
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 = z32.decode(senderDeviceId);
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.TArray<T.TString>;
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.TArray<T.TString>;
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.TArray<T.TString>;
60
+ mapShareUrls: T.TUnsafe<readonly [string, ...string[]]>;
61
61
  mapShareCreatedAt: T.TNumber;
62
62
  }>, T.TObject<{
63
63
  mapId: T.TString;
@@ -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,qBAIvB,CAAA;AACF,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"}
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.2",
3
+ "version": "1.0.0-pre.4",
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
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert'
2
2
  import { once } from 'node:events'
3
3
  import http from 'node:http'
4
- import { type AddressInfo } from 'node:net'
4
+ import { type AddressInfo, type Socket } from 'node:net'
5
5
 
6
6
  import { createServerAdapter } from '@whatwg-node/server'
7
7
  import pDefer from 'p-defer'
@@ -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'
@@ -17,6 +16,7 @@ import { RootRouter } from './routes/root.js'
17
16
  import type { FetchContext } from './types.js'
18
17
 
19
18
  export { errors } from './lib/errors.js'
19
+ export { CUSTOM_MAP_ID, DEFAULT_MAP_ID } from './lib/constants.js'
20
20
 
21
21
  export type {
22
22
  MapInfo,
@@ -57,7 +57,7 @@ export function createServer(options: ServerOptions) {
57
57
  options.keyPair = Agent.keyPair()
58
58
  }
59
59
 
60
- const deferredListen = pDefer<ListenResult>()
60
+ let deferredListen = pDefer<ListenResult>()
61
61
  const context = new Context({
62
62
  ...options,
63
63
  keyPair: options.keyPair,
@@ -79,7 +79,7 @@ export function createServer(options: ServerOptions) {
79
79
  serverAdapter(req, res, {
80
80
  isLocalhost: false,
81
81
  // @ts-expect-error - the types for this are too hard and making them work would not add any type safety.
82
- remoteDeviceId: z32.encode(req.socket.remotePublicKey),
82
+ remoteDeviceId: Buffer.from(req.socket.remotePublicKey).toString('hex'),
83
83
  })
84
84
  })
85
85
  const secretStreamServer = createSecretStreamServer(remoteHttpServer, {
@@ -87,8 +87,8 @@ export function createServer(options: ServerOptions) {
87
87
  })
88
88
 
89
89
  // Track connections for proper cleanup
90
- const connections = new Set<any>()
91
- const onConnection = (socket: any) => {
90
+ const connections = new Set<Socket>()
91
+ const onConnection = (socket: Socket) => {
92
92
  connections.add(socket)
93
93
  socket.once('close', () => {
94
94
  connections.delete(socket)
@@ -125,6 +125,8 @@ export function createServer(options: ServerOptions) {
125
125
  once(localHttpServer, 'close'),
126
126
  once(secretStreamServer, 'close'),
127
127
  ])
128
+ // Reset deferred listen for potential restart with different ports
129
+ deferredListen = pDefer<ListenResult>()
128
130
  },
129
131
  }
130
132
  }
@@ -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 = z32.decode(this.#state.senderDeviceId)
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}`,
@@ -68,7 +67,7 @@ export class DownloadRequest extends TypedEventTarget<
68
67
  this.#updateState({ status: 'canceled' })
69
68
  } else if (getErrorCode(error)) {
70
69
  // Specific known error from the server
71
- this.#updateState({ status: 'error', error })
70
+ this.#updateState({ status: 'error', error: jsonError(error) })
72
71
  } else {
73
72
  // Once the download has started, the sender can only close the
74
73
  // connection to cancel the download, which we only see as an
@@ -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
  })
@@ -5,7 +5,7 @@ import {
5
5
  type DownloadStateUpdate,
6
6
  type MapInfo,
7
7
  } from '../types.js'
8
- import { errors } from './errors.js'
8
+ import { errors, jsonError } from './errors.js'
9
9
  import { StateUpdateEvent } from './state-update-event.js'
10
10
  import { addTrailingSlash, generateId, getErrorCode } from './utils.js'
11
11
 
@@ -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',
@@ -155,7 +155,7 @@ export class DownloadResponse extends TypedEventTarget<
155
155
  } else if (getErrorCode(error) === 'ECONNRESET') {
156
156
  this.#updateState({ status: 'aborted' })
157
157
  } else {
158
- this.#updateState({ status: 'error', error })
158
+ this.#updateState({ status: 'error', error: jsonError(error) })
159
159
  }
160
160
  })
161
161
 
@@ -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 (!Array.isArray(urls)) {
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 z32.encode(randomBytes(8))
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
+ }
@@ -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 = z32.decode(senderDeviceId)
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(requestUrl: string, remotePort: number): string[] {
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.Array(T.String({ format: 'uri' }), {
77
- minItems: 1,
78
- description:
79
- 'List of map share URLs (for each network interface of the sharer)',
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',