@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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type RequestHandler, type IRequestStrict, type IRequest } from 'itty-router'
|
|
2
|
+
import { Type as T, type StaticType } from 'typebox'
|
|
3
|
+
import { Compile } from 'typebox/compile'
|
|
4
|
+
|
|
5
|
+
import { errors } from '../lib/errors.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A small helper to create middleware that parses and validates the request
|
|
9
|
+
* body against the given schema. Downstream handlers can access the type-safe
|
|
10
|
+
* parsed body via `request.parsed`.
|
|
11
|
+
*/
|
|
12
|
+
export const parseRequest = <
|
|
13
|
+
TSchema extends T.TSchema,
|
|
14
|
+
TRequest extends IRequest = IRequestStrict,
|
|
15
|
+
>(
|
|
16
|
+
schema: TSchema,
|
|
17
|
+
): RequestHandler<
|
|
18
|
+
TRequest & { parsed: StaticType<[], 'Decode', {}, {}, TSchema> }
|
|
19
|
+
> => {
|
|
20
|
+
const C = Compile(schema)
|
|
21
|
+
return async (request) => {
|
|
22
|
+
try {
|
|
23
|
+
const json = await request.json()
|
|
24
|
+
// Use Check to validate without type coercion
|
|
25
|
+
if (!C.Check(json)) {
|
|
26
|
+
throw new errors.INVALID_REQUEST()
|
|
27
|
+
}
|
|
28
|
+
request.parsed = json as StaticType<[], 'Decode', {}, {}, TSchema>
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if ('status' in (error as object)) throw error
|
|
31
|
+
throw new errors.INVALID_REQUEST()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { IttyRouter } from 'itty-router'
|
|
2
|
+
import { Type as T, type Static } from 'typebox'
|
|
3
|
+
|
|
4
|
+
import type { Context } from '../context.js'
|
|
5
|
+
import { CUSTOM_MAP_ID } from '../lib/constants.js'
|
|
6
|
+
import { DownloadRequest } from '../lib/download-request.js'
|
|
7
|
+
import { errors } from '../lib/errors.js'
|
|
8
|
+
import { createEventStreamResponse } from '../lib/event-stream-response.js'
|
|
9
|
+
import { SelfEvictingTimeoutMap } from '../lib/self-evicting-map.js'
|
|
10
|
+
import { addTrailingSlash } from '../lib/utils.js'
|
|
11
|
+
import { parseRequest } from '../middlewares/parse-request.js'
|
|
12
|
+
import {
|
|
13
|
+
MapShareUrls,
|
|
14
|
+
EstimatedSizeBytes,
|
|
15
|
+
ShareId,
|
|
16
|
+
type RouterExternal,
|
|
17
|
+
} from '../types.js'
|
|
18
|
+
|
|
19
|
+
const DownloadCreateRequest = T.Object({
|
|
20
|
+
senderDeviceId: T.String({
|
|
21
|
+
minLength: 1,
|
|
22
|
+
description: 'The ID of the device that is sending the map share',
|
|
23
|
+
}),
|
|
24
|
+
mapShareUrls: MapShareUrls,
|
|
25
|
+
shareId: ShareId,
|
|
26
|
+
estimatedSizeBytes: EstimatedSizeBytes,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export type DownloadCreateParams = Static<typeof DownloadCreateRequest>
|
|
30
|
+
|
|
31
|
+
export function DownloadsRouter(
|
|
32
|
+
{ base }: { base: string },
|
|
33
|
+
ctx: Context,
|
|
34
|
+
): RouterExternal {
|
|
35
|
+
const downloads = new SelfEvictingTimeoutMap<string, DownloadRequest>()
|
|
36
|
+
const router = IttyRouter({ base })
|
|
37
|
+
|
|
38
|
+
router.post('/', parseRequest(DownloadCreateRequest), async (request) => {
|
|
39
|
+
const writable = ctx.createMapWritableStream(CUSTOM_MAP_ID)
|
|
40
|
+
const download = new DownloadRequest(
|
|
41
|
+
writable,
|
|
42
|
+
request.parsed,
|
|
43
|
+
ctx.getKeyPair(),
|
|
44
|
+
)
|
|
45
|
+
downloads.set(download.state.downloadId, download)
|
|
46
|
+
return Response.json(download.state, {
|
|
47
|
+
status: 201,
|
|
48
|
+
headers: {
|
|
49
|
+
Location: new URL(
|
|
50
|
+
download.state.downloadId,
|
|
51
|
+
addTrailingSlash(request.url),
|
|
52
|
+
).href,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
router.get('/', () => {
|
|
58
|
+
return Array.from(downloads.values()).map((d) => d.state)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
router.get('/:downloadId', async (request) => {
|
|
62
|
+
return getDownload(request.params.downloadId).state
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
router.get('/:downloadId/events', async (request): Promise<Response> => {
|
|
66
|
+
const download = getDownload(request.params.downloadId)
|
|
67
|
+
return createEventStreamResponse(download, { signal: request.signal })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
router.post('/:downloadId/abort', async (request): Promise<Response> => {
|
|
71
|
+
const download = getDownload(request.params.downloadId)
|
|
72
|
+
if (download.state.status !== 'downloading') {
|
|
73
|
+
throw new errors.ABORT_NOT_DOWNLOADING(
|
|
74
|
+
`Cannot abort: download status is '${download.state.status}'`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
download.cancel()
|
|
78
|
+
return new Response(null, { status: 204 })
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return router
|
|
82
|
+
|
|
83
|
+
function getDownload(downloadId: string): DownloadRequest {
|
|
84
|
+
const download = downloads.get(downloadId)
|
|
85
|
+
if (!download) {
|
|
86
|
+
throw new errors.DOWNLOAD_NOT_FOUND(
|
|
87
|
+
`Download ID not found: ${downloadId}`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return download
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
|
|
3
|
+
import { IRequestStrict, IttyRouter, type RequestHandler } from 'itty-router'
|
|
4
|
+
import {
|
|
5
|
+
fetch as secretStreamFetch,
|
|
6
|
+
Agent as SecretStreamAgent,
|
|
7
|
+
} from 'secret-stream-http'
|
|
8
|
+
import { Type as T, type Static } from 'typebox'
|
|
9
|
+
import { Compile } from 'typebox/compile'
|
|
10
|
+
import z32 from 'z32'
|
|
11
|
+
|
|
12
|
+
import type { Context } from '../context.js'
|
|
13
|
+
import { errors, StatusError } from '../lib/errors.js'
|
|
14
|
+
import { createEventStreamResponse } from '../lib/event-stream-response.js'
|
|
15
|
+
import { MapShare } from '../lib/map-share.js'
|
|
16
|
+
import { SelfEvictingTimeoutMap } from '../lib/self-evicting-map.js'
|
|
17
|
+
import { addTrailingSlash, timingSafeEqual } from '../lib/utils.js'
|
|
18
|
+
import { localhostOnly } from '../middlewares/localhost-only.js'
|
|
19
|
+
import { parseRequest } from '../middlewares/parse-request.js'
|
|
20
|
+
import {
|
|
21
|
+
MapShareUrls,
|
|
22
|
+
MapShareDeclineReason,
|
|
23
|
+
MapShareState,
|
|
24
|
+
type FetchContext,
|
|
25
|
+
type RouterExternal,
|
|
26
|
+
} from '../types.js'
|
|
27
|
+
|
|
28
|
+
const MapShareCreateRequest = T.Object({
|
|
29
|
+
mapId: T.String({ minLength: 1 }),
|
|
30
|
+
receiverDeviceId: T.String({ minLength: 1 }),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type MapShareCreateParams = Static<typeof MapShareCreateRequest>
|
|
34
|
+
|
|
35
|
+
const LocalMapShareDeclineRequest = T.Object({
|
|
36
|
+
reason: MapShareDeclineReason,
|
|
37
|
+
mapShareUrls: MapShareUrls,
|
|
38
|
+
senderDeviceId: T.String({
|
|
39
|
+
minLength: 1,
|
|
40
|
+
description: 'The ID of the device that is sending the map share',
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export type MapShareDeclineParams = Static<typeof LocalMapShareDeclineRequest>
|
|
45
|
+
|
|
46
|
+
const RemoteMapShareDeclineRequest = T.Object({
|
|
47
|
+
reason: MapShareDeclineReason,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const CompiledLocalMapShareDeclineRequest = Compile(LocalMapShareDeclineRequest)
|
|
51
|
+
const CompiledRemoteMapShareDeclineRequest = Compile(
|
|
52
|
+
RemoteMapShareDeclineRequest,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
export function MapSharesRouter(
|
|
56
|
+
{ base }: { base: string },
|
|
57
|
+
ctx: Context,
|
|
58
|
+
): RouterExternal {
|
|
59
|
+
const mapShares = new SelfEvictingTimeoutMap<string, MapShare>()
|
|
60
|
+
|
|
61
|
+
const router = IttyRouter<IRequestStrict, [FetchContext]>({ base })
|
|
62
|
+
|
|
63
|
+
// These routes are only accessible from localhost (local API)
|
|
64
|
+
|
|
65
|
+
router.post(
|
|
66
|
+
'/',
|
|
67
|
+
localhostOnly,
|
|
68
|
+
parseRequest(MapShareCreateRequest),
|
|
69
|
+
async (request) => {
|
|
70
|
+
const { mapId, receiverDeviceId } = request.parsed
|
|
71
|
+
const mapInfo = await ctx.getMapInfo(mapId)
|
|
72
|
+
const mapShare = new MapShare({
|
|
73
|
+
...mapInfo,
|
|
74
|
+
receiverDeviceId,
|
|
75
|
+
baseUrls: getRemoteBaseUrls(request.url, await ctx.getRemotePort()),
|
|
76
|
+
})
|
|
77
|
+
mapShares.set(mapShare.shareId, mapShare)
|
|
78
|
+
return Response.json(mapShare.state, {
|
|
79
|
+
status: 201,
|
|
80
|
+
headers: {
|
|
81
|
+
Location: new URL(mapShare.shareId, addTrailingSlash(request.url))
|
|
82
|
+
.href,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
router.get('/', localhostOnly, () => {
|
|
89
|
+
return Array.from(mapShares.values()).map((ms) => ms.state)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
router.get(
|
|
93
|
+
'/:shareId/events',
|
|
94
|
+
localhostOnly,
|
|
95
|
+
async (request): Promise<Response> => {
|
|
96
|
+
const mapShare = getMapShare(request.params.shareId)
|
|
97
|
+
return createEventStreamResponse(mapShare, { signal: request.signal })
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
router.post(
|
|
102
|
+
'/:shareId/cancel',
|
|
103
|
+
localhostOnly,
|
|
104
|
+
async (request): Promise<Response> => {
|
|
105
|
+
const mapShare = getMapShare(request.params.shareId)
|
|
106
|
+
mapShare.cancel()
|
|
107
|
+
return new Response(null, { status: 204 })
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// These routes can be accessed by a remote peer, but the peer deviceId must
|
|
112
|
+
// match the receiverDeviceId on the map share
|
|
113
|
+
|
|
114
|
+
const validateRemoteDeviceId = async (
|
|
115
|
+
request: IRequestStrict,
|
|
116
|
+
{ remoteDeviceId, isLocalhost }: FetchContext,
|
|
117
|
+
) => {
|
|
118
|
+
if (isLocalhost) return
|
|
119
|
+
if (!remoteDeviceId) {
|
|
120
|
+
throw new errors.FORBIDDEN()
|
|
121
|
+
}
|
|
122
|
+
const mapShare = getMapShare(request.params.shareId)
|
|
123
|
+
if (!timingSafeEqual(remoteDeviceId, mapShare.state.receiverDeviceId)) {
|
|
124
|
+
throw new errors.FORBIDDEN()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
router.all('/:shareId', validateRemoteDeviceId)
|
|
129
|
+
router.all('/:shareId/*', validateRemoteDeviceId)
|
|
130
|
+
|
|
131
|
+
router.get('/:shareId', async (request): Promise<MapShareState> => {
|
|
132
|
+
return getMapShare(request.params.shareId).state
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
router.get('/:shareId/download', async (request): Promise<Response> => {
|
|
136
|
+
const mapShare = getMapShare(request.params.shareId)
|
|
137
|
+
const stream = ctx.createMapReadableStream(mapShare.state.mapId)
|
|
138
|
+
return mapShare.downloadResponse(stream)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const localDeclineHandler: RequestHandler = async (request) => {
|
|
142
|
+
let parsedBody: Static<typeof LocalMapShareDeclineRequest>
|
|
143
|
+
try {
|
|
144
|
+
const json = await request.json()
|
|
145
|
+
if (!CompiledLocalMapShareDeclineRequest.Check(json)) {
|
|
146
|
+
throw new errors.INVALID_REQUEST()
|
|
147
|
+
}
|
|
148
|
+
parsedBody = json
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if ('status' in (err as object)) throw err
|
|
151
|
+
throw new errors.INVALID_REQUEST()
|
|
152
|
+
}
|
|
153
|
+
const { senderDeviceId, mapShareUrls, reason } = parsedBody
|
|
154
|
+
const remotePublicKey = z32.decode(senderDeviceId)
|
|
155
|
+
const keyPair = ctx.getKeyPair()
|
|
156
|
+
let response: Response | undefined
|
|
157
|
+
// The sharer could have multiple IPs for different network interfaces, and
|
|
158
|
+
// not all of them may be on the same network as us, so try each URL until
|
|
159
|
+
// one works
|
|
160
|
+
for (const mapShareUrl of mapShareUrls) {
|
|
161
|
+
const url = new URL('decline', addTrailingSlash(mapShareUrl))
|
|
162
|
+
try {
|
|
163
|
+
response = (await secretStreamFetch(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
body: JSON.stringify({ reason }),
|
|
166
|
+
signal: request.signal,
|
|
167
|
+
dispatcher: new SecretStreamAgent({ remotePublicKey, keyPair }),
|
|
168
|
+
})) as unknown as Response // Subtle difference bewteen Undici fetch Response and whatwg Response
|
|
169
|
+
break // Exit loop on successful fetch
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
172
|
+
throw error // Handle abort in caller
|
|
173
|
+
}
|
|
174
|
+
// Otherwise, try the next URL
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!response) {
|
|
178
|
+
throw new errors.DECLINE_CANNOT_CONNECT()
|
|
179
|
+
}
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
// pass through error from sender
|
|
182
|
+
throw new StatusError(response.status, await response.json())
|
|
183
|
+
}
|
|
184
|
+
return new Response(null, { status: 204 })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const remoteDeclineHandler: RequestHandler = async (request) => {
|
|
188
|
+
let parsedBody: Static<typeof RemoteMapShareDeclineRequest>
|
|
189
|
+
try {
|
|
190
|
+
const json = await request.json()
|
|
191
|
+
if (!CompiledRemoteMapShareDeclineRequest.Check(json)) {
|
|
192
|
+
throw new errors.INVALID_REQUEST()
|
|
193
|
+
}
|
|
194
|
+
parsedBody = json
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if ('status' in (err as object)) throw err
|
|
197
|
+
throw new errors.INVALID_REQUEST()
|
|
198
|
+
}
|
|
199
|
+
const { reason } = parsedBody
|
|
200
|
+
const mapShare = getMapShare(request.params.shareId)
|
|
201
|
+
mapShare.decline(reason)
|
|
202
|
+
return new Response(null, { status: 204 })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
router.post(
|
|
206
|
+
'/:shareId/decline',
|
|
207
|
+
async (request, { isLocalhost }): Promise<Response> => {
|
|
208
|
+
if (isLocalhost) {
|
|
209
|
+
return localDeclineHandler(request)
|
|
210
|
+
} else {
|
|
211
|
+
return remoteDeclineHandler(request)
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return router
|
|
217
|
+
|
|
218
|
+
function getMapShare(shareId: string) {
|
|
219
|
+
const mapShare = mapShares.get(shareId)
|
|
220
|
+
if (!mapShare) {
|
|
221
|
+
throw new errors.MAP_SHARE_NOT_FOUND(`Map share ID not found: ${shareId}`)
|
|
222
|
+
}
|
|
223
|
+
return mapShare
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the base URLs for downloads for all non-internal IPv4 addresses of the machine
|
|
229
|
+
*/
|
|
230
|
+
function getRemoteBaseUrls(requestUrl: string, remotePort: number): string[] {
|
|
231
|
+
requestUrl = addTrailingSlash(requestUrl)
|
|
232
|
+
const interfaces = os.networkInterfaces()
|
|
233
|
+
const baseUrls: string[] = []
|
|
234
|
+
for (const iface of Object.values(interfaces)) {
|
|
235
|
+
if (!iface) continue
|
|
236
|
+
for (const addr of iface) {
|
|
237
|
+
if (addr.family === 'IPv4' && !addr.internal) {
|
|
238
|
+
const url = new URL(requestUrl)
|
|
239
|
+
url.hostname = addr.address
|
|
240
|
+
url.port = remotePort.toString()
|
|
241
|
+
baseUrls.push(url.toString())
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return baseUrls
|
|
246
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { IRequestStrict, IttyRouter, type RequestHandler } from 'itty-router'
|
|
2
|
+
import Mutex from 'p-mutex'
|
|
3
|
+
import { createServer as createSmpServer } from 'styled-map-package/server'
|
|
4
|
+
|
|
5
|
+
import type { Context } from '../context.js'
|
|
6
|
+
import {
|
|
7
|
+
CUSTOM_MAP_ID,
|
|
8
|
+
DEFAULT_MAP_ID,
|
|
9
|
+
FALLBACK_MAP_ID,
|
|
10
|
+
} from '../lib/constants.js'
|
|
11
|
+
import { errors } from '../lib/errors.js'
|
|
12
|
+
import { addTrailingSlash, noop } from '../lib/utils.js'
|
|
13
|
+
|
|
14
|
+
type MapRequest = IRequestStrict & {
|
|
15
|
+
params: {
|
|
16
|
+
mapId: string
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function MapsRouter({ base = '/' }, ctx: Context) {
|
|
21
|
+
base = addTrailingSlash(base)
|
|
22
|
+
const uploadMutexes = new Map<string, Mutex>()
|
|
23
|
+
|
|
24
|
+
const smpServer = createSmpServer({
|
|
25
|
+
base: `${base}:mapId/`,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const router = IttyRouter<IRequestStrict>({ base })
|
|
29
|
+
|
|
30
|
+
router.get<MapRequest>(`/:mapId/info`, async (request) => {
|
|
31
|
+
const info = await ctx.getMapInfo(request.params.mapId)
|
|
32
|
+
return {
|
|
33
|
+
created: info.mapCreated,
|
|
34
|
+
size: info.estimatedSizeBytes,
|
|
35
|
+
name: info.mapName,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const uploadHandler: RequestHandler<MapRequest> = async (request) => {
|
|
40
|
+
const writable = ctx.createMapWritableStream(request.params.mapId)
|
|
41
|
+
if (!request.body) {
|
|
42
|
+
throw new errors.INVALID_REQUEST('Request body is required')
|
|
43
|
+
}
|
|
44
|
+
await request.body.pipeTo(writable)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
router.put<MapRequest>('/:mapId', async (request) => {
|
|
48
|
+
// Only allow uploading to the custom map ID for now
|
|
49
|
+
if (
|
|
50
|
+
request.params.mapId === DEFAULT_MAP_ID ||
|
|
51
|
+
request.params.mapId === FALLBACK_MAP_ID
|
|
52
|
+
) {
|
|
53
|
+
throw new errors.FORBIDDEN(
|
|
54
|
+
`Uploading to map ID "${request.params.mapId}" is not allowed`,
|
|
55
|
+
)
|
|
56
|
+
} else if (request.params.mapId !== CUSTOM_MAP_ID) {
|
|
57
|
+
throw new errors.MAP_NOT_FOUND(`Map not found: ${request.params.mapId}`)
|
|
58
|
+
}
|
|
59
|
+
if (!request.body) {
|
|
60
|
+
throw new errors.INVALID_REQUEST('Request body is required')
|
|
61
|
+
}
|
|
62
|
+
// Get or create a mutex for this mapId to ensure sequential uploads
|
|
63
|
+
let mutex = uploadMutexes.get(request.params.mapId)
|
|
64
|
+
if (!mutex) {
|
|
65
|
+
mutex = new Mutex()
|
|
66
|
+
uploadMutexes.set(request.params.mapId, mutex)
|
|
67
|
+
}
|
|
68
|
+
await mutex.withLock(() => uploadHandler(request))
|
|
69
|
+
return new Response(null, { status: 200 })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
router.delete<MapRequest>('/:mapId', async (request) => {
|
|
73
|
+
// Only allow deleting the custom map ID
|
|
74
|
+
if (
|
|
75
|
+
request.params.mapId === DEFAULT_MAP_ID ||
|
|
76
|
+
request.params.mapId === FALLBACK_MAP_ID
|
|
77
|
+
) {
|
|
78
|
+
throw new errors.FORBIDDEN(
|
|
79
|
+
`Deleting the map ID "${request.params.mapId}" is not allowed`,
|
|
80
|
+
)
|
|
81
|
+
} else if (request.params.mapId !== CUSTOM_MAP_ID) {
|
|
82
|
+
throw new errors.MAP_NOT_FOUND(`Map not found: ${request.params.mapId}`)
|
|
83
|
+
}
|
|
84
|
+
// Use mutex to wait for any active uploads to complete before deleting
|
|
85
|
+
let mutex = uploadMutexes.get(request.params.mapId)
|
|
86
|
+
if (!mutex) {
|
|
87
|
+
mutex = new Mutex()
|
|
88
|
+
uploadMutexes.set(request.params.mapId, mutex)
|
|
89
|
+
}
|
|
90
|
+
await mutex.withLock(() => ctx.deleteMap(request.params.mapId))
|
|
91
|
+
return new Response(null, { status: 204 })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
router.all(`/:mapId/*`, async (request) => {
|
|
95
|
+
if (request.params.mapId === DEFAULT_MAP_ID) {
|
|
96
|
+
return defaultMapHandler(request)
|
|
97
|
+
}
|
|
98
|
+
// Get the reader first - this throws MAP_NOT_FOUND for unknown map IDs
|
|
99
|
+
const reader = await ctx.getReader(request.params.mapId)
|
|
100
|
+
try {
|
|
101
|
+
return await smpServer.fetch(request, reader)
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// Convert generic 404 from smpServer to RESOURCE_NOT_FOUND
|
|
104
|
+
if (err instanceof Error && 'status' in err && err.status === 404) {
|
|
105
|
+
throw new errors.RESOURCE_NOT_FOUND()
|
|
106
|
+
}
|
|
107
|
+
throw err
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Special handler for the default map ID that tries to serve a custom map
|
|
112
|
+
// if available, otherwise falls back to the online style or bundled fallback
|
|
113
|
+
const defaultMapHandler: RequestHandler = async (request) => {
|
|
114
|
+
const defaultOnlineStyleUrl = ctx.getDefaultOnlineStyleUrl()
|
|
115
|
+
const styleUrls = [
|
|
116
|
+
new URL(`../${CUSTOM_MAP_ID}/style.json`, request.url),
|
|
117
|
+
defaultOnlineStyleUrl,
|
|
118
|
+
new URL(`../${FALLBACK_MAP_ID}/style.json`, request.url),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for (const url of styleUrls) {
|
|
122
|
+
let response: Response | void
|
|
123
|
+
if (url === defaultOnlineStyleUrl) {
|
|
124
|
+
response = await fetch(url).catch(noop)
|
|
125
|
+
} else {
|
|
126
|
+
// No need to go through the networking stack for local requests
|
|
127
|
+
response = await router.fetch(new Request(url)).catch(noop)
|
|
128
|
+
}
|
|
129
|
+
response?.body?.cancel() // Close the connection
|
|
130
|
+
if (response && response.ok) {
|
|
131
|
+
return new Response(null, {
|
|
132
|
+
status: 302,
|
|
133
|
+
headers: {
|
|
134
|
+
location: url.toString(),
|
|
135
|
+
'access-control-allow-origin': '*',
|
|
136
|
+
'cache-control': 'no-cache',
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new errors.MAP_NOT_FOUND('No available map style found')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return router
|
|
146
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { json, Router, type IRequestStrict } from 'itty-router'
|
|
2
|
+
|
|
3
|
+
import type { Context } from '../context.js'
|
|
4
|
+
import { error } from '../lib/errors.js'
|
|
5
|
+
import { localhostOnly } from '../middlewares/localhost-only.js'
|
|
6
|
+
import type { FetchContext, RouterExternal } from '../types.js'
|
|
7
|
+
import { DownloadsRouter } from './downloads.js'
|
|
8
|
+
import { MapSharesRouter } from './map-shares.js'
|
|
9
|
+
import { MapsRouter } from './maps.js'
|
|
10
|
+
|
|
11
|
+
const MAPS_BASE = '/maps/'
|
|
12
|
+
const MAP_SHARES_BASE = '/mapShares/'
|
|
13
|
+
const DOWNLOADS_BASE = '/downloads/'
|
|
14
|
+
|
|
15
|
+
export function RootRouter({ base = '/' }, ctx: Context): RouterExternal {
|
|
16
|
+
const router = Router<IRequestStrict, [FetchContext]>({
|
|
17
|
+
base,
|
|
18
|
+
// The `error` handler will send a response with the status code from any
|
|
19
|
+
// thrown StatusError, or a 500 for any other errors.
|
|
20
|
+
catch: (err) => error(err),
|
|
21
|
+
// Sends a 404 response for any requests that don't match a route, and for
|
|
22
|
+
// any request handlers that return JSON will send a JSON response.
|
|
23
|
+
finally: [(response) => response ?? error(404), json],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const mapsRouter = MapsRouter({ base: MAPS_BASE }, ctx)
|
|
27
|
+
const downloadsRouter = DownloadsRouter({ base: DOWNLOADS_BASE }, ctx)
|
|
28
|
+
const mapSharesRouter = MapSharesRouter({ base: MAP_SHARES_BASE }, ctx)
|
|
29
|
+
|
|
30
|
+
router.all(`${MAPS_BASE}*`, localhostOnly, mapsRouter.fetch)
|
|
31
|
+
router.all(`${DOWNLOADS_BASE}*`, localhostOnly, downloadsRouter.fetch)
|
|
32
|
+
// Some map share routes are remote-accessible - localhostOnly is applied in
|
|
33
|
+
// the map shares router where needed
|
|
34
|
+
router.all(`${MAP_SHARES_BASE}*`, mapSharesRouter.fetch)
|
|
35
|
+
|
|
36
|
+
return router
|
|
37
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { RequestLike } from 'itty-router'
|
|
2
|
+
import { Type as T, type Static } from 'typebox'
|
|
3
|
+
|
|
4
|
+
export const MapShareDeclineReason = T.Union([
|
|
5
|
+
T.Literal('disk_full', {
|
|
6
|
+
description:
|
|
7
|
+
"The map share was declined because the receiver's disk is full",
|
|
8
|
+
}),
|
|
9
|
+
T.Literal('user_rejected', {
|
|
10
|
+
description: 'The map share was declined by the user',
|
|
11
|
+
}),
|
|
12
|
+
T.String({
|
|
13
|
+
minLength: 1,
|
|
14
|
+
description: 'Other reason for declining the map share',
|
|
15
|
+
}),
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
const MapShareStateUpdate = T.Union([
|
|
19
|
+
T.Object({
|
|
20
|
+
status: T.Literal('pending', {
|
|
21
|
+
description: 'Map share is awaiting a response',
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
T.Object({
|
|
25
|
+
status: T.Literal('declined', {
|
|
26
|
+
description: 'Map share has been declined',
|
|
27
|
+
}),
|
|
28
|
+
reason: MapShareDeclineReason,
|
|
29
|
+
}),
|
|
30
|
+
T.Object({
|
|
31
|
+
status: T.Literal('downloading', {
|
|
32
|
+
description: 'Map share is currently being downloaded',
|
|
33
|
+
}),
|
|
34
|
+
bytesDownloaded: T.Number({
|
|
35
|
+
description:
|
|
36
|
+
'Total bytes downloaded so far (compare with estimatedSizeBytes for progress)',
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
T.Object({
|
|
40
|
+
status: T.Literal('canceled', {
|
|
41
|
+
description: 'Map share has been canceled (by the sharer)',
|
|
42
|
+
}),
|
|
43
|
+
}),
|
|
44
|
+
T.Object({
|
|
45
|
+
status: T.Literal('aborted', {
|
|
46
|
+
description: 'Map share download was aborted (by the receiver)',
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
T.Object({
|
|
50
|
+
status: T.Literal('completed', { description: 'Map has been downloaded' }),
|
|
51
|
+
}),
|
|
52
|
+
T.Object({
|
|
53
|
+
status: T.Literal('error', {
|
|
54
|
+
description: 'An error occurred while downloading',
|
|
55
|
+
}),
|
|
56
|
+
error: T.Object(
|
|
57
|
+
{
|
|
58
|
+
message: T.String({ description: 'Error message' }),
|
|
59
|
+
code: T.String({ description: 'Error code' }),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: 'Error that occurred while receiving the map share',
|
|
63
|
+
},
|
|
64
|
+
),
|
|
65
|
+
}),
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
export type MapShareStateUpdate = Static<typeof MapShareStateUpdate>
|
|
69
|
+
export type MapShareStatus = Static<typeof MapShareStateUpdate>['status']
|
|
70
|
+
|
|
71
|
+
export type DownloadStateUpdate = Extract<
|
|
72
|
+
MapShareStateUpdate,
|
|
73
|
+
{ status: 'downloading' | 'completed' | 'error' | 'canceled' | 'aborted' }
|
|
74
|
+
>
|
|
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
|
+
})
|
|
81
|
+
export const ShareId = T.String({
|
|
82
|
+
minLength: 1,
|
|
83
|
+
description: 'The ID of the map share',
|
|
84
|
+
})
|
|
85
|
+
export type ShareId = Static<typeof ShareId>
|
|
86
|
+
export const EstimatedSizeBytes = T.Number({
|
|
87
|
+
description: 'Estimated size of the map data in bytes',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const MapInfo = T.Object({
|
|
91
|
+
mapId: T.String({ description: 'The ID of the map' }),
|
|
92
|
+
mapName: T.String({ description: 'The name of the map' }),
|
|
93
|
+
estimatedSizeBytes: EstimatedSizeBytes,
|
|
94
|
+
bounds: T.ReadonlyType(
|
|
95
|
+
T.Tuple([T.Number(), T.Number(), T.Number(), T.Number()], {
|
|
96
|
+
description: 'The bounding box of the map data',
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
minzoom: T.Number({ description: 'The minimum zoom level of the map data' }),
|
|
100
|
+
maxzoom: T.Number({ description: 'The maximum zoom level of the map data' }),
|
|
101
|
+
mapCreated: T.Number({
|
|
102
|
+
description: 'Timestamp (ms since epoch) when the map was created',
|
|
103
|
+
}),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const MapShareBase = T.Intersect([
|
|
107
|
+
T.Object({
|
|
108
|
+
receiverDeviceId: T.String({
|
|
109
|
+
description: 'The ID of the device that can receive the map share',
|
|
110
|
+
}),
|
|
111
|
+
shareId: ShareId,
|
|
112
|
+
mapShareUrls: MapShareUrls,
|
|
113
|
+
mapShareCreated: T.Number({
|
|
114
|
+
description: 'Timestamp (ms since epoch) when the map share was created',
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
MapInfo,
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
export const MapShareState = T.Intersect([MapShareBase, MapShareStateUpdate])
|
|
121
|
+
|
|
122
|
+
export type MapShareState = DistributiveIntersection<
|
|
123
|
+
Static<typeof MapShareBase>,
|
|
124
|
+
Static<typeof MapShareStateUpdate>
|
|
125
|
+
>
|
|
126
|
+
|
|
127
|
+
export type MapInfo = Static<typeof MapInfo>
|
|
128
|
+
|
|
129
|
+
export type FetchContext = {
|
|
130
|
+
isLocalhost?: boolean
|
|
131
|
+
remoteDeviceId?: string
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type DistributiveIntersection<Base, Union> = Union extends unknown
|
|
135
|
+
? Base & Union
|
|
136
|
+
: never
|
|
137
|
+
|
|
138
|
+
export type DistributeProperty<T, K extends keyof T> = T[K] extends infer V
|
|
139
|
+
? V extends T[K]
|
|
140
|
+
? { [P in K]: V } & Omit<T, K>
|
|
141
|
+
: never
|
|
142
|
+
: never
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* External router returned by create router functions. Ensures routers are
|
|
146
|
+
* called with the necessary fetch context
|
|
147
|
+
*/
|
|
148
|
+
export type RouterExternal = {
|
|
149
|
+
fetch: (request: RequestLike, context: FetchContext) => Promise<any>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type BBox = Readonly<[number, number, number, number]>
|