@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.
- package/README.md +610 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +181 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +112 -0
- package/dist/lib/constants.d.ts +7 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/download-request.d.ts +17 -0
- package/dist/lib/download-request.d.ts.map +1 -0
- package/dist/lib/download-request.js +113 -0
- package/dist/lib/errors.d.ts +88 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +158 -0
- package/dist/lib/event-stream-response.d.ts +17 -0
- package/dist/lib/event-stream-response.d.ts.map +1 -0
- package/dist/lib/event-stream-response.js +39 -0
- package/dist/lib/event-target.d.ts +9 -0
- package/dist/lib/event-target.d.ts.map +1 -0
- package/dist/lib/event-target.js +4 -0
- package/dist/lib/fetch-api.d.ts +3 -0
- package/dist/lib/fetch-api.d.ts.map +1 -0
- package/dist/lib/fetch-api.js +16 -0
- package/dist/lib/map-share.d.ts +52 -0
- package/dist/lib/map-share.d.ts.map +1 -0
- package/dist/lib/map-share.js +142 -0
- package/dist/lib/secret-stream-fetch.d.ts +7 -0
- package/dist/lib/secret-stream-fetch.d.ts.map +1 -0
- package/dist/lib/secret-stream-fetch.js +34 -0
- package/dist/lib/self-evicting-map.d.ts +16 -0
- package/dist/lib/self-evicting-map.d.ts.map +1 -0
- package/dist/lib/self-evicting-map.js +29 -0
- package/dist/lib/state-update-event.d.ts +8 -0
- package/dist/lib/state-update-event.d.ts.map +1 -0
- package/dist/lib/state-update-event.js +10 -0
- package/dist/lib/utils.d.ts +32 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +96 -0
- package/dist/middlewares/localhost-only.d.ts +11 -0
- package/dist/middlewares/localhost-only.d.ts.map +1 -0
- package/dist/middlewares/localhost-only.js +10 -0
- package/dist/middlewares/parse-request.d.ts +11 -0
- package/dist/middlewares/parse-request.d.ts.map +1 -0
- package/dist/middlewares/parse-request.js +25 -0
- package/dist/routes/downloads.d.ts +15 -0
- package/dist/routes/downloads.d.ts.map +1 -0
- package/dist/routes/downloads.js +60 -0
- package/dist/routes/map-shares.d.ts +19 -0
- package/dist/routes/map-shares.d.ts.map +1 -0
- package/dist/routes/map-shares.js +192 -0
- package/dist/routes/maps.d.ts +6 -0
- package/dist/routes/maps.d.ts.map +1 -0
- package/dist/routes/maps.js +118 -0
- package/dist/routes/root.d.ts +6 -0
- package/dist/routes/root.d.ts.map +1 -0
- package/dist/routes/root.js +29 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +96 -0
- package/node_modules/@envelop/instrumentation/LICENSE +21 -0
- package/node_modules/@envelop/instrumentation/README.md +30 -0
- package/node_modules/@envelop/instrumentation/cjs/index.js +5 -0
- package/node_modules/@envelop/instrumentation/cjs/instrumentation.js +89 -0
- package/node_modules/@envelop/instrumentation/cjs/package.json +1 -0
- package/node_modules/@envelop/instrumentation/esm/index.js +2 -0
- package/node_modules/@envelop/instrumentation/esm/instrumentation.js +82 -0
- package/node_modules/@envelop/instrumentation/package.json +57 -0
- package/node_modules/@envelop/instrumentation/typings/index.d.cts +1 -0
- package/node_modules/@envelop/instrumentation/typings/index.d.ts +1 -0
- package/node_modules/@envelop/instrumentation/typings/instrumentation.d.cts +44 -0
- package/node_modules/@envelop/instrumentation/typings/instrumentation.d.ts +44 -0
- package/node_modules/@whatwg-node/disposablestack/cjs/AsyncDisposableStack.js +73 -0
- package/node_modules/@whatwg-node/disposablestack/cjs/DisposableStack.js +62 -0
- package/node_modules/@whatwg-node/disposablestack/cjs/SupressedError.js +16 -0
- package/node_modules/@whatwg-node/disposablestack/cjs/index.js +11 -0
- package/node_modules/@whatwg-node/disposablestack/cjs/package.json +1 -0
- package/node_modules/@whatwg-node/disposablestack/cjs/symbols.js +20 -0
- package/node_modules/@whatwg-node/disposablestack/cjs/utils.js +11 -0
- package/node_modules/@whatwg-node/disposablestack/esm/AsyncDisposableStack.js +69 -0
- package/node_modules/@whatwg-node/disposablestack/esm/DisposableStack.js +58 -0
- package/node_modules/@whatwg-node/disposablestack/esm/SupressedError.js +12 -0
- package/node_modules/@whatwg-node/disposablestack/esm/index.js +7 -0
- package/node_modules/@whatwg-node/disposablestack/esm/symbols.js +16 -0
- package/node_modules/@whatwg-node/disposablestack/esm/utils.js +7 -0
- package/node_modules/@whatwg-node/disposablestack/package.json +44 -0
- package/node_modules/@whatwg-node/disposablestack/typings/AsyncDisposableStack.d.cts +15 -0
- package/node_modules/@whatwg-node/disposablestack/typings/AsyncDisposableStack.d.ts +15 -0
- package/node_modules/@whatwg-node/disposablestack/typings/DisposableStack.d.cts +14 -0
- package/node_modules/@whatwg-node/disposablestack/typings/DisposableStack.d.ts +14 -0
- package/node_modules/@whatwg-node/disposablestack/typings/SupressedError.d.cts +5 -0
- package/node_modules/@whatwg-node/disposablestack/typings/SupressedError.d.ts +5 -0
- package/node_modules/@whatwg-node/disposablestack/typings/index.d.cts +4 -0
- package/node_modules/@whatwg-node/disposablestack/typings/index.d.ts +4 -0
- package/node_modules/@whatwg-node/disposablestack/typings/symbols.d.cts +5 -0
- package/node_modules/@whatwg-node/disposablestack/typings/symbols.d.ts +5 -0
- package/node_modules/@whatwg-node/disposablestack/typings/utils.d.cts +2 -0
- package/node_modules/@whatwg-node/disposablestack/typings/utils.d.ts +2 -0
- package/node_modules/@whatwg-node/promise-helpers/cjs/index.js +270 -0
- package/node_modules/@whatwg-node/promise-helpers/cjs/package.json +1 -0
- package/node_modules/@whatwg-node/promise-helpers/esm/index.js +257 -0
- package/node_modules/@whatwg-node/promise-helpers/package.json +43 -0
- package/node_modules/@whatwg-node/promise-helpers/typings/index.d.cts +31 -0
- package/node_modules/@whatwg-node/promise-helpers/typings/index.d.ts +31 -0
- package/node_modules/@whatwg-node/server/README.md +590 -0
- package/node_modules/@whatwg-node/server/cjs/createServerAdapter.js +368 -0
- package/node_modules/@whatwg-node/server/cjs/index.js +17 -0
- package/node_modules/@whatwg-node/server/cjs/package.json +1 -0
- package/node_modules/@whatwg-node/server/cjs/plugins/types.js +0 -0
- package/node_modules/@whatwg-node/server/cjs/plugins/useContentEncoding.js +73 -0
- package/node_modules/@whatwg-node/server/cjs/plugins/useCors.js +124 -0
- package/node_modules/@whatwg-node/server/cjs/plugins/useErrorHandling.js +52 -0
- package/node_modules/@whatwg-node/server/cjs/types.js +0 -0
- package/node_modules/@whatwg-node/server/cjs/utils.js +599 -0
- package/node_modules/@whatwg-node/server/cjs/uwebsockets.js +241 -0
- package/node_modules/@whatwg-node/server/esm/createServerAdapter.js +365 -0
- package/node_modules/@whatwg-node/server/esm/index.js +11 -0
- package/node_modules/@whatwg-node/server/esm/plugins/types.js +0 -0
- package/node_modules/@whatwg-node/server/esm/plugins/useContentEncoding.js +70 -0
- package/node_modules/@whatwg-node/server/esm/plugins/useCors.js +120 -0
- package/node_modules/@whatwg-node/server/esm/plugins/useErrorHandling.js +46 -0
- package/node_modules/@whatwg-node/server/esm/types.js +0 -0
- package/node_modules/@whatwg-node/server/esm/utils.js +588 -0
- package/node_modules/@whatwg-node/server/esm/uwebsockets.js +234 -0
- package/node_modules/@whatwg-node/server/package.json +46 -0
- package/node_modules/@whatwg-node/server/typings/createServerAdapter.d.cts +19 -0
- package/node_modules/@whatwg-node/server/typings/createServerAdapter.d.ts +19 -0
- package/node_modules/@whatwg-node/server/typings/index.d.cts +11 -0
- package/node_modules/@whatwg-node/server/typings/index.d.ts +11 -0
- package/node_modules/@whatwg-node/server/typings/plugins/types.d.cts +76 -0
- package/node_modules/@whatwg-node/server/typings/plugins/types.d.ts +76 -0
- package/node_modules/@whatwg-node/server/typings/plugins/useContentEncoding.d.cts +2 -0
- package/node_modules/@whatwg-node/server/typings/plugins/useContentEncoding.d.ts +2 -0
- package/node_modules/@whatwg-node/server/typings/plugins/useCors.d.cts +14 -0
- package/node_modules/@whatwg-node/server/typings/plugins/useCors.d.ts +14 -0
- package/node_modules/@whatwg-node/server/typings/plugins/useErrorHandling.d.cts +13 -0
- package/node_modules/@whatwg-node/server/typings/plugins/useErrorHandling.d.ts +13 -0
- package/node_modules/@whatwg-node/server/typings/types.d.cts +100 -0
- package/node_modules/@whatwg-node/server/typings/types.d.ts +100 -0
- package/node_modules/@whatwg-node/server/typings/utils.d.cts +42 -0
- package/node_modules/@whatwg-node/server/typings/utils.d.ts +42 -0
- package/node_modules/@whatwg-node/server/typings/uwebsockets.d.cts +32 -0
- package/node_modules/@whatwg-node/server/typings/uwebsockets.d.ts +32 -0
- package/node_modules/tslib/CopyrightNotice.txt +15 -0
- package/node_modules/tslib/LICENSE.txt +12 -0
- package/node_modules/tslib/README.md +164 -0
- package/node_modules/tslib/SECURITY.md +41 -0
- package/node_modules/tslib/modules/index.d.ts +38 -0
- package/node_modules/tslib/modules/index.js +70 -0
- package/node_modules/tslib/modules/package.json +3 -0
- package/node_modules/tslib/package.json +47 -0
- package/node_modules/tslib/tslib.d.ts +460 -0
- package/node_modules/tslib/tslib.es6.html +1 -0
- package/node_modules/tslib/tslib.es6.js +402 -0
- package/node_modules/tslib/tslib.es6.mjs +401 -0
- package/node_modules/tslib/tslib.html +1 -0
- package/node_modules/tslib/tslib.js +484 -0
- package/package.json +87 -0
- package/src/context.ts +203 -0
- package/src/index.ts +193 -0
- package/src/lib/constants.ts +6 -0
- package/src/lib/download-request.ts +142 -0
- package/src/lib/errors.ts +187 -0
- package/src/lib/event-stream-response.ts +57 -0
- package/src/lib/event-target.ts +11 -0
- package/src/lib/fetch-api.ts +18 -0
- package/src/lib/map-share.ts +185 -0
- package/src/lib/secret-stream-fetch.ts +42 -0
- package/src/lib/self-evicting-map.ts +35 -0
- package/src/lib/state-update-event.ts +14 -0
- package/src/lib/utils.ts +110 -0
- package/src/middlewares/localhost-only.ts +16 -0
- package/src/middlewares/parse-request.ts +34 -0
- package/src/routes/downloads.ts +92 -0
- package/src/routes/map-shares.ts +246 -0
- package/src/routes/maps.ts +146 -0
- package/src/routes/root.ts +37 -0
- 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"}
|