@comapeo/map-server 1.0.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +610 -0
  2. package/dist/context.d.ts +46 -0
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/context.js +181 -0
  5. package/dist/index.d.ts +25 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +112 -0
  8. package/dist/lib/constants.d.ts +7 -0
  9. package/dist/lib/constants.d.ts.map +1 -0
  10. package/dist/lib/constants.js +6 -0
  11. package/dist/lib/download-request.d.ts +17 -0
  12. package/dist/lib/download-request.d.ts.map +1 -0
  13. package/dist/lib/download-request.js +113 -0
  14. package/dist/lib/errors.d.ts +88 -0
  15. package/dist/lib/errors.d.ts.map +1 -0
  16. package/dist/lib/errors.js +158 -0
  17. package/dist/lib/event-stream-response.d.ts +17 -0
  18. package/dist/lib/event-stream-response.d.ts.map +1 -0
  19. package/dist/lib/event-stream-response.js +39 -0
  20. package/dist/lib/event-target.d.ts +9 -0
  21. package/dist/lib/event-target.d.ts.map +1 -0
  22. package/dist/lib/event-target.js +4 -0
  23. package/dist/lib/fetch-api.d.ts +3 -0
  24. package/dist/lib/fetch-api.d.ts.map +1 -0
  25. package/dist/lib/fetch-api.js +16 -0
  26. package/dist/lib/map-share.d.ts +52 -0
  27. package/dist/lib/map-share.d.ts.map +1 -0
  28. package/dist/lib/map-share.js +142 -0
  29. package/dist/lib/secret-stream-fetch.d.ts +7 -0
  30. package/dist/lib/secret-stream-fetch.d.ts.map +1 -0
  31. package/dist/lib/secret-stream-fetch.js +34 -0
  32. package/dist/lib/self-evicting-map.d.ts +16 -0
  33. package/dist/lib/self-evicting-map.d.ts.map +1 -0
  34. package/dist/lib/self-evicting-map.js +29 -0
  35. package/dist/lib/state-update-event.d.ts +8 -0
  36. package/dist/lib/state-update-event.d.ts.map +1 -0
  37. package/dist/lib/state-update-event.js +10 -0
  38. package/dist/lib/utils.d.ts +32 -0
  39. package/dist/lib/utils.d.ts.map +1 -0
  40. package/dist/lib/utils.js +96 -0
  41. package/dist/middlewares/localhost-only.d.ts +11 -0
  42. package/dist/middlewares/localhost-only.d.ts.map +1 -0
  43. package/dist/middlewares/localhost-only.js +10 -0
  44. package/dist/middlewares/parse-request.d.ts +11 -0
  45. package/dist/middlewares/parse-request.d.ts.map +1 -0
  46. package/dist/middlewares/parse-request.js +25 -0
  47. package/dist/routes/downloads.d.ts +15 -0
  48. package/dist/routes/downloads.d.ts.map +1 -0
  49. package/dist/routes/downloads.js +60 -0
  50. package/dist/routes/map-shares.d.ts +19 -0
  51. package/dist/routes/map-shares.d.ts.map +1 -0
  52. package/dist/routes/map-shares.js +192 -0
  53. package/dist/routes/maps.d.ts +6 -0
  54. package/dist/routes/maps.d.ts.map +1 -0
  55. package/dist/routes/maps.js +118 -0
  56. package/dist/routes/root.d.ts +6 -0
  57. package/dist/routes/root.d.ts.map +1 -0
  58. package/dist/routes/root.js +29 -0
  59. package/dist/types.d.ts +110 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +96 -0
  62. package/node_modules/@envelop/instrumentation/LICENSE +21 -0
  63. package/node_modules/@envelop/instrumentation/README.md +30 -0
  64. package/node_modules/@envelop/instrumentation/cjs/index.js +5 -0
  65. package/node_modules/@envelop/instrumentation/cjs/instrumentation.js +89 -0
  66. package/node_modules/@envelop/instrumentation/cjs/package.json +1 -0
  67. package/node_modules/@envelop/instrumentation/esm/index.js +2 -0
  68. package/node_modules/@envelop/instrumentation/esm/instrumentation.js +82 -0
  69. package/node_modules/@envelop/instrumentation/package.json +57 -0
  70. package/node_modules/@envelop/instrumentation/typings/index.d.cts +1 -0
  71. package/node_modules/@envelop/instrumentation/typings/index.d.ts +1 -0
  72. package/node_modules/@envelop/instrumentation/typings/instrumentation.d.cts +44 -0
  73. package/node_modules/@envelop/instrumentation/typings/instrumentation.d.ts +44 -0
  74. package/node_modules/@whatwg-node/disposablestack/cjs/AsyncDisposableStack.js +73 -0
  75. package/node_modules/@whatwg-node/disposablestack/cjs/DisposableStack.js +62 -0
  76. package/node_modules/@whatwg-node/disposablestack/cjs/SupressedError.js +16 -0
  77. package/node_modules/@whatwg-node/disposablestack/cjs/index.js +11 -0
  78. package/node_modules/@whatwg-node/disposablestack/cjs/package.json +1 -0
  79. package/node_modules/@whatwg-node/disposablestack/cjs/symbols.js +20 -0
  80. package/node_modules/@whatwg-node/disposablestack/cjs/utils.js +11 -0
  81. package/node_modules/@whatwg-node/disposablestack/esm/AsyncDisposableStack.js +69 -0
  82. package/node_modules/@whatwg-node/disposablestack/esm/DisposableStack.js +58 -0
  83. package/node_modules/@whatwg-node/disposablestack/esm/SupressedError.js +12 -0
  84. package/node_modules/@whatwg-node/disposablestack/esm/index.js +7 -0
  85. package/node_modules/@whatwg-node/disposablestack/esm/symbols.js +16 -0
  86. package/node_modules/@whatwg-node/disposablestack/esm/utils.js +7 -0
  87. package/node_modules/@whatwg-node/disposablestack/package.json +44 -0
  88. package/node_modules/@whatwg-node/disposablestack/typings/AsyncDisposableStack.d.cts +15 -0
  89. package/node_modules/@whatwg-node/disposablestack/typings/AsyncDisposableStack.d.ts +15 -0
  90. package/node_modules/@whatwg-node/disposablestack/typings/DisposableStack.d.cts +14 -0
  91. package/node_modules/@whatwg-node/disposablestack/typings/DisposableStack.d.ts +14 -0
  92. package/node_modules/@whatwg-node/disposablestack/typings/SupressedError.d.cts +5 -0
  93. package/node_modules/@whatwg-node/disposablestack/typings/SupressedError.d.ts +5 -0
  94. package/node_modules/@whatwg-node/disposablestack/typings/index.d.cts +4 -0
  95. package/node_modules/@whatwg-node/disposablestack/typings/index.d.ts +4 -0
  96. package/node_modules/@whatwg-node/disposablestack/typings/symbols.d.cts +5 -0
  97. package/node_modules/@whatwg-node/disposablestack/typings/symbols.d.ts +5 -0
  98. package/node_modules/@whatwg-node/disposablestack/typings/utils.d.cts +2 -0
  99. package/node_modules/@whatwg-node/disposablestack/typings/utils.d.ts +2 -0
  100. package/node_modules/@whatwg-node/promise-helpers/cjs/index.js +270 -0
  101. package/node_modules/@whatwg-node/promise-helpers/cjs/package.json +1 -0
  102. package/node_modules/@whatwg-node/promise-helpers/esm/index.js +257 -0
  103. package/node_modules/@whatwg-node/promise-helpers/package.json +43 -0
  104. package/node_modules/@whatwg-node/promise-helpers/typings/index.d.cts +31 -0
  105. package/node_modules/@whatwg-node/promise-helpers/typings/index.d.ts +31 -0
  106. package/node_modules/@whatwg-node/server/README.md +590 -0
  107. package/node_modules/@whatwg-node/server/cjs/createServerAdapter.js +368 -0
  108. package/node_modules/@whatwg-node/server/cjs/index.js +17 -0
  109. package/node_modules/@whatwg-node/server/cjs/package.json +1 -0
  110. package/node_modules/@whatwg-node/server/cjs/plugins/types.js +0 -0
  111. package/node_modules/@whatwg-node/server/cjs/plugins/useContentEncoding.js +73 -0
  112. package/node_modules/@whatwg-node/server/cjs/plugins/useCors.js +124 -0
  113. package/node_modules/@whatwg-node/server/cjs/plugins/useErrorHandling.js +52 -0
  114. package/node_modules/@whatwg-node/server/cjs/types.js +0 -0
  115. package/node_modules/@whatwg-node/server/cjs/utils.js +599 -0
  116. package/node_modules/@whatwg-node/server/cjs/uwebsockets.js +241 -0
  117. package/node_modules/@whatwg-node/server/esm/createServerAdapter.js +365 -0
  118. package/node_modules/@whatwg-node/server/esm/index.js +11 -0
  119. package/node_modules/@whatwg-node/server/esm/plugins/types.js +0 -0
  120. package/node_modules/@whatwg-node/server/esm/plugins/useContentEncoding.js +70 -0
  121. package/node_modules/@whatwg-node/server/esm/plugins/useCors.js +120 -0
  122. package/node_modules/@whatwg-node/server/esm/plugins/useErrorHandling.js +46 -0
  123. package/node_modules/@whatwg-node/server/esm/types.js +0 -0
  124. package/node_modules/@whatwg-node/server/esm/utils.js +588 -0
  125. package/node_modules/@whatwg-node/server/esm/uwebsockets.js +234 -0
  126. package/node_modules/@whatwg-node/server/package.json +46 -0
  127. package/node_modules/@whatwg-node/server/typings/createServerAdapter.d.cts +19 -0
  128. package/node_modules/@whatwg-node/server/typings/createServerAdapter.d.ts +19 -0
  129. package/node_modules/@whatwg-node/server/typings/index.d.cts +11 -0
  130. package/node_modules/@whatwg-node/server/typings/index.d.ts +11 -0
  131. package/node_modules/@whatwg-node/server/typings/plugins/types.d.cts +76 -0
  132. package/node_modules/@whatwg-node/server/typings/plugins/types.d.ts +76 -0
  133. package/node_modules/@whatwg-node/server/typings/plugins/useContentEncoding.d.cts +2 -0
  134. package/node_modules/@whatwg-node/server/typings/plugins/useContentEncoding.d.ts +2 -0
  135. package/node_modules/@whatwg-node/server/typings/plugins/useCors.d.cts +14 -0
  136. package/node_modules/@whatwg-node/server/typings/plugins/useCors.d.ts +14 -0
  137. package/node_modules/@whatwg-node/server/typings/plugins/useErrorHandling.d.cts +13 -0
  138. package/node_modules/@whatwg-node/server/typings/plugins/useErrorHandling.d.ts +13 -0
  139. package/node_modules/@whatwg-node/server/typings/types.d.cts +100 -0
  140. package/node_modules/@whatwg-node/server/typings/types.d.ts +100 -0
  141. package/node_modules/@whatwg-node/server/typings/utils.d.cts +42 -0
  142. package/node_modules/@whatwg-node/server/typings/utils.d.ts +42 -0
  143. package/node_modules/@whatwg-node/server/typings/uwebsockets.d.cts +32 -0
  144. package/node_modules/@whatwg-node/server/typings/uwebsockets.d.ts +32 -0
  145. package/node_modules/tslib/CopyrightNotice.txt +15 -0
  146. package/node_modules/tslib/LICENSE.txt +12 -0
  147. package/node_modules/tslib/README.md +164 -0
  148. package/node_modules/tslib/SECURITY.md +41 -0
  149. package/node_modules/tslib/modules/index.d.ts +38 -0
  150. package/node_modules/tslib/modules/index.js +70 -0
  151. package/node_modules/tslib/modules/package.json +3 -0
  152. package/node_modules/tslib/package.json +47 -0
  153. package/node_modules/tslib/tslib.d.ts +460 -0
  154. package/node_modules/tslib/tslib.es6.html +1 -0
  155. package/node_modules/tslib/tslib.es6.js +402 -0
  156. package/node_modules/tslib/tslib.es6.mjs +401 -0
  157. package/node_modules/tslib/tslib.html +1 -0
  158. package/node_modules/tslib/tslib.js +484 -0
  159. package/package.json +87 -0
  160. package/src/context.ts +203 -0
  161. package/src/index.ts +193 -0
  162. package/src/lib/constants.ts +6 -0
  163. package/src/lib/download-request.ts +142 -0
  164. package/src/lib/errors.ts +187 -0
  165. package/src/lib/event-stream-response.ts +57 -0
  166. package/src/lib/event-target.ts +11 -0
  167. package/src/lib/fetch-api.ts +18 -0
  168. package/src/lib/map-share.ts +185 -0
  169. package/src/lib/secret-stream-fetch.ts +42 -0
  170. package/src/lib/self-evicting-map.ts +35 -0
  171. package/src/lib/state-update-event.ts +14 -0
  172. package/src/lib/utils.ts +110 -0
  173. package/src/middlewares/localhost-only.ts +16 -0
  174. package/src/middlewares/parse-request.ts +34 -0
  175. package/src/routes/downloads.ts +92 -0
  176. package/src/routes/map-shares.ts +246 -0
  177. package/src/routes/maps.ts +146 -0
  178. package/src/routes/root.ts +37 -0
  179. package/src/types.ts +152 -0
package/README.md ADDED
@@ -0,0 +1,610 @@
1
+ # @comapeo/map-server
2
+
3
+ A lightweight embedded map tile server for serving offline vector maps in desktop and mobile applications. Designed primarily for [CoMapeo](https://comapeo.app/), an offline-first mapping tool built for Indigenous communities and land defenders to document and monitor their territories.
4
+
5
+ ## What It Does
6
+
7
+ This server provides everything needed to display offline vector maps in [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/) and compatible libraries:
8
+
9
+ - **Vector tile serving** - Delivers map tiles on demand
10
+ - **MapLibre style.json** - Provides the style specification
11
+ - **Glyphs (fonts)** - Serves text rendering resources
12
+ - **Sprites** - Delivers map icons and symbols
13
+ - **P2P map sharing** - Securely share offline maps between devices on a local network
14
+
15
+ All map resources are served from [Styled Map Package (SMP)](https://github.com/digidem/styled-map-package) files - a zip-based format containing everything needed for a complete offline map.
16
+
17
+ ## Why This Exists
18
+
19
+ CoMapeo and similar offline mapping applications need to:
20
+
21
+ 1. **Serve maps offline** - Display vector maps without internet connectivity
22
+ 2. **Run embedded** - Operate within desktop and mobile apps (Electron, React Native, etc.)
23
+ 3. **Share maps locally** - Transfer large map files between devices on the same network without internet
24
+
25
+ This server solves all three by embedding a lightweight HTTP server that speaks the MapLibre/Mapbox protocol and adds encrypted peer-to-peer map sharing.
26
+
27
+ ## Architecture
28
+
29
+ The server listens on two different network interfaces:
30
+
31
+ ```
32
+ ┌─────────────────────────────────────────────────────────────────┐
33
+ │ Application (CoMapeo, etc) │
34
+ │ │
35
+ │ HTTP Map Server │
36
+ │ ┌─────────────────────────────────────────────────────────────┐│
37
+ │ │ ││
38
+ │ │ Loopback (127.0.0.1) All Interfaces (0.0.0.0) ││
39
+ │ │ • Map tiles • Noise protocol encrypted ││
40
+ │ │ • Styles/glyphs/sprites • Public key authentication ││
41
+ │ │ • Map management API • Map sharing only ││
42
+ │ │ • Regular HTTP • Device-to-device ││
43
+ │ │ ││
44
+ │ └──────────────────────────────────────────────────────────────┘│
45
+ │ ↑ ↑ │
46
+ └─────────┼───────────────────────────────────────┼────────────────┘
47
+ │ │
48
+ MapLibre GL Other Devices
49
+ Your App Code (Noise encrypted streams)
50
+ ```
51
+
52
+ **Loopback Interface** (127.0.0.1): Regular HTTP for your application to serve map tiles and control the server
53
+
54
+ **Network Interface** (0.0.0.0): Only accepts Noise protocol encrypted streams via [secret-stream-http](https://github.com/holepunchto/secret-stream-http). The public keys exchanged during the Noise handshake authenticate both client and server, eliminating the need for TLS certificates.
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ npm install @comapeo/map-server
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ ```javascript
65
+ import { createServer } from '@comapeo/map-server'
66
+ import Hypercore from 'hypercore'
67
+
68
+ // Generate a keypair for this device (persist this!)
69
+ const keyPair = Hypercore.keyPair()
70
+
71
+ const server = createServer({
72
+ // Fallback for when offline maps aren't available
73
+ defaultOnlineStyleUrl: 'https://demotiles.maplibre.org/style.json',
74
+
75
+ // Path to your custom offline map (SMP format)
76
+ customMapPath: '/path/to/custom-map.smp',
77
+
78
+ // Path to bundled fallback map
79
+ fallbackMapPath: '/path/to/fallback-map.smp',
80
+
81
+ // Device keypair for encrypted P2P connections
82
+ keyPair: {
83
+ publicKey: keyPair.publicKey,
84
+ secretKey: keyPair.secretKey,
85
+ },
86
+ })
87
+
88
+ // Start listening on both interfaces
89
+ const { localPort, remotePort } = await server.listen({
90
+ localPort: 8080, // Optional: loopback interface port
91
+ remotePort: 9090, // Optional: network interface port
92
+ })
93
+
94
+ console.log(`Map tiles: http://127.0.0.1:${localPort}/maps/default/style.json`)
95
+ console.log(`P2P sharing: listening on 0.0.0.0:${remotePort} (Noise encrypted)`)
96
+ ```
97
+
98
+ ## Using Maps in MapLibre
99
+
100
+ Once the server is running, configure MapLibre to use it:
101
+
102
+ ```javascript
103
+ import maplibregl from 'maplibre-gl'
104
+
105
+ const map = new maplibregl.Map({
106
+ container: 'map',
107
+ style: 'http://127.0.0.1:8080/maps/default/style.json',
108
+ center: [0, 0],
109
+ zoom: 2,
110
+ })
111
+ ```
112
+
113
+ The `default` map ID provides intelligent fallback:
114
+
115
+ 1. Tries to serve the custom map
116
+ 2. Falls back to the online style URL (if network available)
117
+ 3. Falls back to the bundled offline map
118
+
119
+ ## Map Configuration
120
+
121
+ The server uses a three-tier map system to ensure maps are always available:
122
+
123
+ ### 1. Online Map (Default)
124
+
125
+ By default, when you first start the server, it serves an online map via `defaultOnlineStyleUrl`. This provides global coverage when internet connectivity is available.
126
+
127
+ ### 2. Fallback Map (Always Available Offline)
128
+
129
+ A basic global map that ships with the application, typically the [CoMapeo Fallback Map](https://github.com/digidem/comapeo-fallback-smp). This provides:
130
+
131
+ - Country outlines and borders
132
+ - Major cities and populated places
133
+ - Basic road network
134
+ - Coastlines and major water bodies
135
+
136
+ The fallback map ensures users always have some map coverage even without internet or a custom map.
137
+
138
+ ### 3. Custom Map (Optional)
139
+
140
+ Users can optionally create or upload a detailed offline map for their specific area of interest using the Styled Map Package (SMP) format. Custom maps typically contain:
141
+
142
+ - High-detail vector tiles for a specific region
143
+ - Custom styling optimized for the use case
144
+ - Detailed features like trails, buildings, land use, etc.
145
+ - Much higher zoom levels than the fallback map
146
+
147
+ **Fallback Logic:**
148
+
149
+ When a client requests `/maps/default/style.json`, the server tries sources in this order:
150
+
151
+ 1. **Custom map** - If uploaded by the user
152
+ 2. **Online map** - If internet connectivity is available
153
+ 3. **Fallback map** - Always available as last resort
154
+
155
+ This ensures maps work offline while providing the best available map for the current situation.
156
+
157
+ ## Map Format: Styled Map Package (SMP)
158
+
159
+ SMP files are zip archives containing all resources for a complete offline map:
160
+
161
+ - Vector or raster tiles
162
+ - MapLibre style.json
163
+ - Glyphs (font files for text rendering)
164
+ - Sprite images and metadata (map icons)
165
+
166
+ **Creating SMP files:**
167
+
168
+ - [SMP Downloader](https://styled-map-package.fly.dev/) - Web-based tool
169
+ - [styled-map-package](https://github.com/digidem/styled-map-package) - CLI utilities
170
+ - [mapgl-tile-renderer](https://github.com/ConservationMetrics/mapgl-tile-renderer) - Generate styled raster tiles
171
+
172
+ ## API Reference
173
+
174
+ ### Map Tile API (Localhost Only)
175
+
176
+ All endpoints are prefixed with `http://127.0.0.1:{localPort}`
177
+
178
+ #### Get Map Style
179
+
180
+ ```http
181
+ GET /maps/{mapId}/style.json
182
+ ```
183
+
184
+ Returns the MapLibre style specification. Use this as the `style` URL in MapLibre.
185
+
186
+ **Map IDs:**
187
+
188
+ - `default` - Intelligent fallback (custom → online → fallback)
189
+ - `custom` - Your uploaded custom map
190
+ - `fallback` - Bundled offline map
191
+
192
+ #### Get Tiles
193
+
194
+ ```http
195
+ GET /maps/{mapId}/{z}/{x}/{y}.{format}
196
+ ```
197
+
198
+ Standard slippy map tile endpoint. Format is usually `pbf` for vector tiles or `png`/`jpg` for raster.
199
+
200
+ #### Get Glyphs (Fonts)
201
+
202
+ ```http
203
+ GET /maps/{mapId}/glyphs/{fontstack}/{range}.pbf
204
+ ```
205
+
206
+ Serves font glyphs for text rendering.
207
+
208
+ #### Get Sprites (Icons)
209
+
210
+ ```http
211
+ GET /maps/{mapId}/sprites/{spriteId}{scale}.{format}
212
+ ```
213
+
214
+ Serves sprite images and metadata for map icons.
215
+
216
+ #### Upload Custom Map
217
+
218
+ ```http
219
+ PUT /maps/custom
220
+ Content-Type: application/octet-stream
221
+
222
+ [binary SMP file data]
223
+ ```
224
+
225
+ Uploads a new custom map or replaces an existing one. The map becomes immediately available at `/maps/custom/`. This is how users add detailed offline maps for their specific area of interest.
226
+
227
+ #### Delete Custom Map
228
+
229
+ ```http
230
+ DELETE /maps/custom
231
+ ```
232
+
233
+ Deletes the custom map. Returns 204 No Content on success, 404 if the map doesn't exist. Only the custom map can be deleted - the fallback map is protected.
234
+
235
+ After deletion, the `/maps/default/` endpoint will fall back to the online map or fallback map.
236
+
237
+ #### Get Map Info
238
+
239
+ ```http
240
+ GET /maps/{mapId}/info
241
+ ```
242
+
243
+ Returns metadata about the map.
244
+
245
+ **Response:**
246
+
247
+ ```json
248
+ {
249
+ "name": "Custom Map",
250
+ "size": 12345678,
251
+ "created": 1234567890123
252
+ }
253
+ ```
254
+
255
+ ### P2P Map Sharing API
256
+
257
+ The sharing API allows devices on the same local network to securely transfer SMP files.
258
+
259
+ #### Creating a Share (Sender)
260
+
261
+ ```http
262
+ POST /mapShares
263
+ Content-Type: application/json
264
+
265
+ {
266
+ "mapId": "custom",
267
+ "receiverDeviceId": "kmx8sejfn..." // z32-encoded public key
268
+ }
269
+ ```
270
+
271
+ Creates a share offer for a specific device.
272
+
273
+ **Response (201):**
274
+
275
+ ```json
276
+ {
277
+ "shareId": "abc123...",
278
+ "receiverDeviceId": "kmx8sejfn...",
279
+ "mapId": "custom",
280
+ "mapName": "My Custom Map",
281
+ "mapShareUrls": [
282
+ "http://192.168.1.100:9090/mapShares/abc123...",
283
+ "http://10.0.0.5:9090/mapShares/abc123..."
284
+ ],
285
+ "bounds": [-122.5, 37.5, -122.0, 38.0],
286
+ "minzoom": 0,
287
+ "maxzoom": 14,
288
+ "estimatedSizeBytes": 12345678,
289
+ "status": "pending"
290
+ }
291
+ ```
292
+
293
+ The `mapShareUrls` contain all local IP addresses of the sender. The receiver tries each until one succeeds.
294
+
295
+ #### Monitor Share Progress (Sender)
296
+
297
+ ```http
298
+ GET /mapShares/{shareId}/events
299
+ Accept: text/event-stream
300
+ ```
301
+
302
+ Server-Sent Events stream for real-time status updates.
303
+
304
+ **Statuses:**
305
+
306
+ - `pending` - Awaiting receiver response
307
+ - `downloading` - Receiver is downloading (includes `bytesDownloaded`)
308
+ - `completed` - Transfer finished
309
+ - `declined` - Receiver declined (includes `reason`)
310
+ - `canceled` - Sender canceled
311
+ - `aborted` - Receiver aborted the download
312
+ - `error` - Transfer failed (includes `error`)
313
+
314
+ #### Cancel Share (Sender)
315
+
316
+ ```http
317
+ POST /mapShares/{shareId}/cancel
318
+ ```
319
+
320
+ Returns 204 No Content.
321
+
322
+ #### Starting a Download (Receiver)
323
+
324
+ ```http
325
+ POST /downloads
326
+ Content-Type: application/json
327
+
328
+ {
329
+ "senderDeviceId": "z32-encoded-public-key",
330
+ "shareId": "abc123...",
331
+ "mapShareUrls": ["http://192.168.1.100:9090/mapShares/abc123..."],
332
+ "estimatedSizeBytes": 12345678
333
+ }
334
+ ```
335
+
336
+ Starts downloading a shared map.
337
+
338
+ **Response (201):**
339
+
340
+ ```json
341
+ {
342
+ "downloadId": "xyz789...",
343
+ "status": "downloading",
344
+ "bytesDownloaded": 0,
345
+ "estimatedSizeBytes": 12345678
346
+ }
347
+ ```
348
+
349
+ #### Monitor Download Progress (Receiver)
350
+
351
+ ```http
352
+ GET /downloads/{downloadId}/events
353
+ Accept: text/event-stream
354
+ ```
355
+
356
+ Real-time download progress via Server-Sent Events.
357
+
358
+ #### Abort Download (Receiver)
359
+
360
+ ```http
361
+ POST /downloads/{downloadId}/abort
362
+ ```
363
+
364
+ Returns 204 No Content.
365
+
366
+ #### Decline Share (Receiver)
367
+
368
+ ```http
369
+ POST /mapShares/{shareId}/decline
370
+ Content-Type: application/json
371
+
372
+ {
373
+ "senderDeviceId": "z32-encoded-public-key",
374
+ "mapShareUrls": ["http://192.168.1.100:9090/mapShares/abc123..."],
375
+ "reason": "disk_full" | "user_rejected" | "other reason"
376
+ }
377
+ ```
378
+
379
+ Called on the receiver's local server. The server handles making the P2P request to the sender to notify them of the decline. Returns 204 No Content on success.
380
+
381
+ ## Complete Example: Sharing Between Two Devices
382
+
383
+ ### Device A (Sender)
384
+
385
+ ```javascript
386
+ import { createServer } from '@comapeo/map-server'
387
+ import Hypercore from 'hypercore'
388
+ import z32 from 'z32'
389
+
390
+ const deviceAKeyPair = Hypercore.keyPair()
391
+ const serverA = createServer({
392
+ defaultOnlineStyleUrl: 'https://demotiles.maplibre.org/style.json',
393
+ customMapPath: 'file:///maps/my-map.smp',
394
+ fallbackMapPath: 'file:///maps/fallback.smp',
395
+ keyPair: deviceAKeyPair,
396
+ })
397
+
398
+ const { localPort } = await serverA.listen()
399
+
400
+ // Device B's public key (exchanged via your discovery mechanism)
401
+ const deviceBId = 'kmx8sejfn...' // z32-encoded
402
+
403
+ // Create share
404
+ const res = await fetch(`http://127.0.0.1:${localPort}/mapShares`, {
405
+ method: 'POST',
406
+ headers: { 'Content-Type': 'application/json' },
407
+ body: JSON.stringify({
408
+ mapId: 'custom',
409
+ receiverDeviceId: deviceBId,
410
+ }),
411
+ })
412
+
413
+ const share = await res.json()
414
+
415
+ // Send share offer to Device B via your messaging layer
416
+ await yourApp.sendToDevice(deviceBId, {
417
+ type: 'map-share-offer',
418
+ share,
419
+ })
420
+
421
+ // Monitor progress
422
+ const eventSource = new EventSource(
423
+ `http://127.0.0.1:${localPort}/mapShares/${share.shareId}/events`,
424
+ )
425
+
426
+ eventSource.onmessage = (event) => {
427
+ const state = JSON.parse(event.data)
428
+ if (state.status === 'downloading') {
429
+ console.log(
430
+ `Progress: ${((state.bytesDownloaded / state.estimatedSizeBytes) * 100).toFixed(1)}%`,
431
+ )
432
+ }
433
+ if (state.status === 'completed') {
434
+ console.log('Transfer complete!')
435
+ eventSource.close()
436
+ }
437
+ }
438
+ ```
439
+
440
+ ### Device B (Receiver)
441
+
442
+ ```javascript
443
+ import { createServer } from '@comapeo/map-server'
444
+ import Hypercore from 'hypercore'
445
+
446
+ const deviceBKeyPair = Hypercore.keyPair()
447
+ const serverB = createServer({
448
+ defaultOnlineStyleUrl: 'https://demotiles.maplibre.org/style.json',
449
+ customMapPath: 'file:///maps/my-map.smp',
450
+ fallbackMapPath: 'file:///maps/fallback.smp',
451
+ keyPair: deviceBKeyPair,
452
+ })
453
+
454
+ const { localPort } = await serverB.listen()
455
+
456
+ // Receive share offer from Device A
457
+ yourApp.onMessage(async (message) => {
458
+ if (message.type !== 'map-share-offer') return
459
+
460
+ const { share } = message
461
+
462
+ // Ask user
463
+ const accept = await askUser(
464
+ `Accept "${share.mapName}"? (${formatBytes(share.estimatedSizeBytes)})`,
465
+ )
466
+
467
+ if (!accept) {
468
+ // Decline via local server (server handles P2P request to sender)
469
+ await fetch(
470
+ `http://127.0.0.1:${localPort}/mapShares/${share.shareId}/decline`,
471
+ {
472
+ method: 'POST',
473
+ headers: { 'Content-Type': 'application/json' },
474
+ body: JSON.stringify({
475
+ senderDeviceId: message.senderDeviceId,
476
+ mapShareUrls: share.mapShareUrls,
477
+ reason: 'user_rejected',
478
+ }),
479
+ },
480
+ )
481
+ return
482
+ }
483
+
484
+ // Start download
485
+ const res = await fetch(`http://127.0.0.1:${localPort}/downloads`, {
486
+ method: 'POST',
487
+ headers: { 'Content-Type': 'application/json' },
488
+ body: JSON.stringify({
489
+ senderDeviceId: message.senderDeviceId,
490
+ shareId: share.shareId,
491
+ mapShareUrls: share.mapShareUrls,
492
+ estimatedSizeBytes: share.estimatedSizeBytes,
493
+ }),
494
+ })
495
+
496
+ const download = await res.json()
497
+
498
+ // Monitor download
499
+ const es = new EventSource(
500
+ `http://127.0.0.1:${localPort}/downloads/${download.downloadId}/events`,
501
+ )
502
+
503
+ es.onmessage = (event) => {
504
+ const state = JSON.parse(event.data)
505
+ if (state.status === 'downloading') {
506
+ updateUI((state.bytesDownloaded / state.estimatedSizeBytes) * 100)
507
+ }
508
+ if (state.status === 'completed') {
509
+ console.log('Map ready! Available at /maps/custom/')
510
+ es.close()
511
+ }
512
+ }
513
+ })
514
+ ```
515
+
516
+ ## Security Model
517
+
518
+ - **Localhost API**: Only accessible from `127.0.0.1` - your application code
519
+ - **Noise Protocol Encryption**: The network interface uses the [Noise protocol](http://www.noiseprotocol.org/) via [secret-stream-http](https://github.com/holepunchto/secret-stream-http)
520
+ - **Public Key Authentication**: Client and server public keys from the Noise handshake are used to authenticate connections - no TLS certificates needed
521
+ - **Device Authorization**: Each share is tied to a specific receiver device ID (public key)
522
+ - **Access Validation**: Remote requests are rejected unless the authenticated client public key matches the share's `receiverDeviceId`
523
+
524
+ ## Errors
525
+
526
+ All error responses follow this format:
527
+
528
+ ```json
529
+ {
530
+ "code": "ERROR_CODE",
531
+ "message": "Human-readable error message"
532
+ }
533
+ ```
534
+
535
+ ### Map Errors
536
+
537
+ | Code | Status | Description |
538
+ | -------------------- | ------ | ------------------------------------------------------------------------ |
539
+ | `MAP_NOT_FOUND` | 404 | The requested map does not exist |
540
+ | `RESOURCE_NOT_FOUND` | 404 | The map exists but the requested resource (tile, sprite, glyph) does not |
541
+ | `INVALID_MAP_FILE` | 400 | The uploaded file is not a valid SMP file |
542
+
543
+ ### Map Share Errors (Sender-side)
544
+
545
+ | Code | Status | Description |
546
+ | ----------------------------- | ------ | -------------------------------------------------------- |
547
+ | `MAP_SHARE_NOT_FOUND` | 404 | The requested map share does not exist |
548
+ | `CANCEL_SHARE_NOT_CANCELABLE` | 409 | Cannot cancel a share that is not pending or downloading |
549
+ | `DECLINE_SHARE_NOT_PENDING` | 409 | Cannot decline a share that is not pending |
550
+ | `DECLINE_CANNOT_CONNECT` | 502 | Unable to connect to the sender to decline the share |
551
+
552
+ ### Download Errors (Receiver-side)
553
+
554
+ | Code | Status | Description |
555
+ | ---------------------------- | ------ | ---------------------------------------------------------- |
556
+ | `DOWNLOAD_NOT_FOUND` | 404 | The requested download does not exist |
557
+ | `DOWNLOAD_ERROR` | 500 | The download failed unexpectedly |
558
+ | `DOWNLOAD_SHARE_CANCELED` | 409 | The sender canceled the share before download completed |
559
+ | `DOWNLOAD_SHARE_DECLINED` | 409 | Cannot download a share that was declined |
560
+ | `DOWNLOAD_SHARE_NOT_PENDING` | 409 | Cannot download a share that is not pending |
561
+ | `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 |
563
+
564
+ ### Generic Errors
565
+
566
+ | Code | Status | Description |
567
+ | ----------------- | ------ | ----------------------------------------------------------- |
568
+ | `FORBIDDEN` | 403 | Access denied (remote request without valid authentication) |
569
+ | `INVALID_REQUEST` | 400 | The request body is missing or malformed |
570
+
571
+ ## Network Discovery
572
+
573
+ This library **does not** handle peer discovery. You need to implement that separately:
574
+
575
+ - **mDNS/Bonjour** - Discover devices on local network
576
+ - **Hyperswarm** - DHT-based peer discovery
577
+ - **QR codes** - Scan to exchange device IDs and IP addresses
578
+ - **Manual entry** - Let users type IP addresses
579
+
580
+ The sender provides all their local IP addresses in `mapShareUrls`. The receiver tries each until one connects.
581
+
582
+ ## Use Cases
583
+
584
+ - **CoMapeo** - Offline mapping for Indigenous communities and land defenders
585
+ - **Field data collection** - ODK, Kobo Collect, Terrastories
586
+ - **Offline navigation** - Apps needing offline vector maps
587
+ - **Emergency response** - Maps in areas with poor/no connectivity
588
+ - **Research expeditions** - Scientific fieldwork in remote areas
589
+
590
+ ## Related Projects
591
+
592
+ - [CoMapeo](https://comapeo.app/) - Offline-first mapping for territorial monitoring
593
+ - [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/) - Open-source vector map rendering
594
+ - [Styled Map Package](https://github.com/digidem/styled-map-package) - SMP format specification
595
+ - [CoMapeo Fallback Map](https://github.com/digidem/comapeo-fallback-smp) - Basic global map with country outlines and major cities
596
+ - [secret-stream-http](https://github.com/holepunchto/secret-stream-http) - Encrypted HTTP over TCP
597
+
598
+ ## License
599
+
600
+ MIT
601
+
602
+ ---
603
+
604
+ **Sources:**
605
+
606
+ - [CoMapeo: Introducing CoMapeo](https://awana.digital/blog/introducing-comapeo----a-next-gen-territorial-monitoring-mapping-collaboration-tool)
607
+ - [MapLibre GL JS Documentation](https://maplibre.org/maplibre-gl-js/docs/)
608
+ - [Styled Map Package on GitHub](https://github.com/digidem/styled-map-package)
609
+ - [SMP Downloader Tool](https://styled-map-package.fly.dev/)
610
+ - [MapGL Tile Renderer](https://github.com/ConservationMetrics/mapgl-tile-renderer)
@@ -0,0 +1,46 @@
1
+ import { Reader } from 'styled-map-package';
2
+ import type { SetRequired } from 'type-fest';
3
+ import type { ServerOptions } from './index.js';
4
+ type ContextOptions = SetRequired<ServerOptions, 'keyPair'> & {
5
+ getRemotePort: () => Promise<number>;
6
+ };
7
+ export declare class Context {
8
+ #private;
9
+ getRemotePort: () => Promise<number>;
10
+ constructor({ defaultOnlineStyleUrl, customMapPath, fallbackMapPath, keyPair, getRemotePort, }: ContextOptions);
11
+ getDefaultOnlineStyleUrl(): URL;
12
+ getKeyPair(): {
13
+ publicKey: Uint8Array;
14
+ secretKey: Uint8Array;
15
+ };
16
+ getMapInfo(mapId: string): Promise<{
17
+ mapId: string;
18
+ mapName: string;
19
+ bounds: readonly [number, number, number, number];
20
+ maxzoom: number;
21
+ minzoom: number;
22
+ estimatedSizeBytes: number;
23
+ mapCreated: number;
24
+ }>;
25
+ getReader(mapId: string): Promise<Reader>;
26
+ createMapReadableStream(mapId: string): ReadableStream<Uint8Array>;
27
+ /**
28
+ * Creates a writable stream to write map data to the specified map ID.
29
+ * The data is first written to a temporary file, and once the stream is closed,
30
+ * the temporary file replaces the existing map file. This ensures that the map
31
+ * file is only updated when the write operation is fully complete.
32
+ *
33
+ * @param mapId - The ID of the map to write data to.
34
+ * @returns A writable stream to write map data.
35
+ */
36
+ createMapWritableStream(mapId: string): WritableStream<any>;
37
+ /**
38
+ * Deletes the map file for the specified map ID.
39
+ * Closes any existing reader and removes it from the cache.
40
+ *
41
+ * @param mapId - The ID of the map to delete.
42
+ */
43
+ deleteMap(mapId: string): Promise<void>;
44
+ }
45
+ export {};
46
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAW/C,KAAK,cAAc,GAAG,WAAW,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG;IAC7D,aAAa,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACpC,CAAA;AAID,qBAAa,OAAO;;IAKnB,aAAa,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;gBAExB,EACX,qBAAqB,EACrB,aAAa,EACb,eAAe,EACf,OAAO,EACP,aAAa,GACb,EAAE,cAAc;IAmBjB,wBAAwB;IAGxB,UAAU;mBA/Ba,UAAU;mBAAa,UAAU;;IAkClD,UAAU,CAAC,KAAK,EAAE,MAAM;;;;;;;;;IA2B9B,SAAS,CAAC,KAAK,EAAE,MAAM;IAavB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAO/B,cAAc,CAAC,UAAU,CAAC;IAEhC;;;;;;;;OAQG;IACH,uBAAuB,CAAC,KAAK,EAAE,MAAM;IAmDrC;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM;CAuB7B"}