@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
@@ -0,0 +1,96 @@
1
+ import crypto from 'node:crypto';
2
+ import { randomBytes } from 'crypto';
3
+ import z32 from 'z32';
4
+ /**
5
+ * If the argument is an `Error` instance, return its `code` property if it is a string.
6
+ * Otherwise, returns `undefined`.
7
+ *
8
+ * @param {unknown} maybeError
9
+ * @returns {undefined | string}
10
+ * @example
11
+ * try {
12
+ * // do something
13
+ * } catch (err) {
14
+ * console.error(getErrorCode(err))
15
+ * }
16
+ */
17
+ export function getErrorCode(maybeError) {
18
+ if (maybeError instanceof Error &&
19
+ 'code' in maybeError &&
20
+ typeof maybeError.code === 'string') {
21
+ return maybeError.code;
22
+ }
23
+ return undefined;
24
+ }
25
+ export function noop() { }
26
+ export function generateId() {
27
+ return z32.encode(randomBytes(8));
28
+ }
29
+ export function getOrInsert(map, key, value) {
30
+ if (map.has(key)) {
31
+ return map.get(key);
32
+ }
33
+ map.set(key, value);
34
+ return value;
35
+ }
36
+ export function timingSafeEqual(a, b) {
37
+ const aBuf = Buffer.from(a);
38
+ const bBuf = Buffer.from(b);
39
+ if (aBuf.length !== bBuf.length) {
40
+ return false;
41
+ }
42
+ return crypto.timingSafeEqual(aBuf, bBuf);
43
+ }
44
+ /**
45
+ * Returns a bbox that is the smallest bounding box that contains all the input bboxes.
46
+ *
47
+ * @param bboxes
48
+ * @returns Bounding Box [w, s, e, n] of all input bboxes
49
+ */
50
+ export function unionBBox(bboxes) {
51
+ let [w, s, e, n] = bboxes[0];
52
+ for (let i = 1; i < bboxes.length; i++) {
53
+ const [w1, s1, e1, n1] = bboxes[i];
54
+ w = Math.min(w, w1);
55
+ s = Math.min(s, s1);
56
+ e = Math.max(e, e1);
57
+ n = Math.max(n, n1);
58
+ }
59
+ return [w, s, e, n];
60
+ }
61
+ export function getStyleBbox(style) {
62
+ const sourceBboxes = [];
63
+ for (const source of Object.values(style.sources)) {
64
+ if (!('bounds' in source))
65
+ continue;
66
+ sourceBboxes.push(source.bounds);
67
+ }
68
+ if (!isNonEmptyArray(sourceBboxes)) {
69
+ return [-180, -85.0511, 180, 85.0511];
70
+ }
71
+ return unionBBox(sourceBboxes);
72
+ }
73
+ export function getStyleMaxZoom(style) {
74
+ let maxzoom = -1;
75
+ for (const source of Object.values(style.sources)) {
76
+ if (!('maxzoom' in source))
77
+ continue;
78
+ maxzoom = Math.max(maxzoom, source.maxzoom ?? -1);
79
+ }
80
+ return maxzoom === -1 ? 22 : maxzoom;
81
+ }
82
+ export function getStyleMinZoom(style) {
83
+ let minzoom = 99;
84
+ for (const source of Object.values(style.sources)) {
85
+ if (!('minzoom' in source))
86
+ continue;
87
+ minzoom = Math.min(minzoom, source.minzoom ?? 99);
88
+ }
89
+ return minzoom === 99 ? 0 : minzoom;
90
+ }
91
+ function isNonEmptyArray(arr) {
92
+ return arr.length > 0;
93
+ }
94
+ export function addTrailingSlash(url) {
95
+ return url.endsWith('/') ? url : url + '/';
96
+ }
@@ -0,0 +1,11 @@
1
+ import { type IRequestStrict, type RequestHandler } from 'itty-router';
2
+ /**
3
+ * Middleware to restrict access to localhost only. The localhost listener must
4
+ * pass { isLocalhost: true } in the context.
5
+ */
6
+ export declare const localhostOnly: RequestHandler<IRequestStrict, [
7
+ {
8
+ isLocalhost: boolean;
9
+ }
10
+ ]>;
11
+ //# sourceMappingURL=localhost-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localhost-only.d.ts","sourceRoot":"","sources":["../../src/middlewares/localhost-only.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAItE;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,cAAc,CACzC,cAAc,EACd;IAAC;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE;CAAC,CAK1B,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { errors } from '../lib/errors.js';
2
+ /**
3
+ * Middleware to restrict access to localhost only. The localhost listener must
4
+ * pass { isLocalhost: true } in the context.
5
+ */
6
+ export const localhostOnly = async (_, { isLocalhost }) => {
7
+ if (!isLocalhost) {
8
+ throw new errors.FORBIDDEN();
9
+ }
10
+ };
@@ -0,0 +1,11 @@
1
+ import { type RequestHandler, type IRequestStrict, type IRequest } from 'itty-router';
2
+ import { Type as T, type StaticType } from 'typebox';
3
+ /**
4
+ * A small helper to create middleware that parses and validates the request
5
+ * body against the given schema. Downstream handlers can access the type-safe
6
+ * parsed body via `request.parsed`.
7
+ */
8
+ export declare const parseRequest: <TSchema extends T.TSchema, TRequest extends IRequest = IRequestStrict>(schema: TSchema) => RequestHandler<TRequest & {
9
+ parsed: StaticType<[], "Decode", {}, {}, TSchema>;
10
+ }>;
11
+ //# sourceMappingURL=parse-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-request.d.ts","sourceRoot":"","sources":["../../src/middlewares/parse-request.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AACrF,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AAKpD;;;;GAIG;AACH,eAAO,MAAM,YAAY,GACxB,OAAO,SAAS,CAAC,CAAC,OAAO,EACzB,QAAQ,SAAS,QAAQ,GAAG,cAAc,EAE1C,QAAQ,OAAO,KACb,cAAc,CAChB,QAAQ,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;CAAE,CAgBhE,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { Compile } from 'typebox/compile';
2
+ import { errors } from '../lib/errors.js';
3
+ /**
4
+ * A small helper to create middleware that parses and validates the request
5
+ * body against the given schema. Downstream handlers can access the type-safe
6
+ * parsed body via `request.parsed`.
7
+ */
8
+ export const parseRequest = (schema) => {
9
+ const C = Compile(schema);
10
+ return async (request) => {
11
+ try {
12
+ const json = await request.json();
13
+ // Use Check to validate without type coercion
14
+ if (!C.Check(json)) {
15
+ throw new errors.INVALID_REQUEST();
16
+ }
17
+ request.parsed = json;
18
+ }
19
+ catch (error) {
20
+ if ('status' in error)
21
+ throw error;
22
+ throw new errors.INVALID_REQUEST();
23
+ }
24
+ };
25
+ };
@@ -0,0 +1,15 @@
1
+ import { Type as T, type Static } from 'typebox';
2
+ import type { Context } from '../context.js';
3
+ import { type RouterExternal } from '../types.js';
4
+ declare const DownloadCreateRequest: T.TObject<{
5
+ senderDeviceId: T.TString;
6
+ mapShareUrls: T.TArray<T.TString>;
7
+ shareId: T.TString;
8
+ estimatedSizeBytes: T.TNumber;
9
+ }>;
10
+ export type DownloadCreateParams = Static<typeof DownloadCreateRequest>;
11
+ export declare function DownloadsRouter({ base }: {
12
+ base: string;
13
+ }, ctx: Context): RouterExternal;
14
+ export {};
15
+ //# sourceMappingURL=downloads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downloads.d.ts","sourceRoot":"","sources":["../../src/routes/downloads.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;AAEhD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAQ5C,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,aAAa,CAAA;AAEpB,QAAA,MAAM,qBAAqB;;;;;EAQzB,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEvE,wBAAgB,eAAe,CAC9B,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1B,GAAG,EAAE,OAAO,GACV,cAAc,CA0DhB"}
@@ -0,0 +1,60 @@
1
+ import { IttyRouter } from 'itty-router';
2
+ import { Type as T } from 'typebox';
3
+ import { CUSTOM_MAP_ID } from '../lib/constants.js';
4
+ import { DownloadRequest } from '../lib/download-request.js';
5
+ import { errors } from '../lib/errors.js';
6
+ import { createEventStreamResponse } from '../lib/event-stream-response.js';
7
+ import { SelfEvictingTimeoutMap } from '../lib/self-evicting-map.js';
8
+ import { addTrailingSlash } from '../lib/utils.js';
9
+ import { parseRequest } from '../middlewares/parse-request.js';
10
+ import { MapShareUrls, EstimatedSizeBytes, ShareId, } from '../types.js';
11
+ const DownloadCreateRequest = T.Object({
12
+ senderDeviceId: T.String({
13
+ minLength: 1,
14
+ description: 'The ID of the device that is sending the map share',
15
+ }),
16
+ mapShareUrls: MapShareUrls,
17
+ shareId: ShareId,
18
+ estimatedSizeBytes: EstimatedSizeBytes,
19
+ });
20
+ export function DownloadsRouter({ base }, ctx) {
21
+ const downloads = new SelfEvictingTimeoutMap();
22
+ const router = IttyRouter({ base });
23
+ router.post('/', parseRequest(DownloadCreateRequest), async (request) => {
24
+ const writable = ctx.createMapWritableStream(CUSTOM_MAP_ID);
25
+ const download = new DownloadRequest(writable, request.parsed, ctx.getKeyPair());
26
+ downloads.set(download.state.downloadId, download);
27
+ return Response.json(download.state, {
28
+ status: 201,
29
+ headers: {
30
+ Location: new URL(download.state.downloadId, addTrailingSlash(request.url)).href,
31
+ },
32
+ });
33
+ });
34
+ router.get('/', () => {
35
+ return Array.from(downloads.values()).map((d) => d.state);
36
+ });
37
+ router.get('/:downloadId', async (request) => {
38
+ return getDownload(request.params.downloadId).state;
39
+ });
40
+ router.get('/:downloadId/events', async (request) => {
41
+ const download = getDownload(request.params.downloadId);
42
+ return createEventStreamResponse(download, { signal: request.signal });
43
+ });
44
+ router.post('/:downloadId/abort', async (request) => {
45
+ const download = getDownload(request.params.downloadId);
46
+ if (download.state.status !== 'downloading') {
47
+ throw new errors.ABORT_NOT_DOWNLOADING(`Cannot abort: download status is '${download.state.status}'`);
48
+ }
49
+ download.cancel();
50
+ return new Response(null, { status: 204 });
51
+ });
52
+ return router;
53
+ function getDownload(downloadId) {
54
+ const download = downloads.get(downloadId);
55
+ if (!download) {
56
+ throw new errors.DOWNLOAD_NOT_FOUND(`Download ID not found: ${downloadId}`);
57
+ }
58
+ return download;
59
+ }
60
+ }
@@ -0,0 +1,19 @@
1
+ import { Type as T, type Static } from 'typebox';
2
+ import type { Context } from '../context.js';
3
+ import { type RouterExternal } from '../types.js';
4
+ declare const MapShareCreateRequest: T.TObject<{
5
+ mapId: T.TString;
6
+ receiverDeviceId: T.TString;
7
+ }>;
8
+ export type MapShareCreateParams = Static<typeof MapShareCreateRequest>;
9
+ declare const LocalMapShareDeclineRequest: T.TObject<{
10
+ reason: T.TUnion<[T.TLiteral<"disk_full">, T.TLiteral<"user_rejected">, T.TString]>;
11
+ mapShareUrls: T.TArray<T.TString>;
12
+ senderDeviceId: T.TString;
13
+ }>;
14
+ export type MapShareDeclineParams = Static<typeof LocalMapShareDeclineRequest>;
15
+ export declare function MapSharesRouter({ base }: {
16
+ base: string;
17
+ }, ctx: Context): RouterExternal;
18
+ export {};
19
+ //# sourceMappingURL=map-shares.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-shares.d.ts","sourceRoot":"","sources":["../../src/routes/map-shares.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;AAIhD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAQ5C,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,aAAa,CAAA;AAEpB,QAAA,MAAM,qBAAqB;;;EAGzB,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEvE,QAAA,MAAM,2BAA2B;;;;EAO/B,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAW9E,wBAAgB,eAAe,CAC9B,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1B,GAAG,EAAE,OAAO,GACV,cAAc,CAuKhB"}
@@ -0,0 +1,192 @@
1
+ import os from 'node:os';
2
+ import { IttyRouter } from 'itty-router';
3
+ import { fetch as secretStreamFetch, Agent as SecretStreamAgent, } from 'secret-stream-http';
4
+ import { Type as T } from 'typebox';
5
+ import { Compile } from 'typebox/compile';
6
+ import z32 from 'z32';
7
+ import { errors, StatusError } from '../lib/errors.js';
8
+ import { createEventStreamResponse } from '../lib/event-stream-response.js';
9
+ import { MapShare } from '../lib/map-share.js';
10
+ import { SelfEvictingTimeoutMap } from '../lib/self-evicting-map.js';
11
+ import { addTrailingSlash, timingSafeEqual } from '../lib/utils.js';
12
+ import { localhostOnly } from '../middlewares/localhost-only.js';
13
+ import { parseRequest } from '../middlewares/parse-request.js';
14
+ import { MapShareUrls, MapShareDeclineReason, } from '../types.js';
15
+ const MapShareCreateRequest = T.Object({
16
+ mapId: T.String({ minLength: 1 }),
17
+ receiverDeviceId: T.String({ minLength: 1 }),
18
+ });
19
+ const LocalMapShareDeclineRequest = T.Object({
20
+ reason: MapShareDeclineReason,
21
+ mapShareUrls: MapShareUrls,
22
+ senderDeviceId: T.String({
23
+ minLength: 1,
24
+ description: 'The ID of the device that is sending the map share',
25
+ }),
26
+ });
27
+ const RemoteMapShareDeclineRequest = T.Object({
28
+ reason: MapShareDeclineReason,
29
+ });
30
+ const CompiledLocalMapShareDeclineRequest = Compile(LocalMapShareDeclineRequest);
31
+ const CompiledRemoteMapShareDeclineRequest = Compile(RemoteMapShareDeclineRequest);
32
+ export function MapSharesRouter({ base }, ctx) {
33
+ const mapShares = new SelfEvictingTimeoutMap();
34
+ const router = IttyRouter({ base });
35
+ // These routes are only accessible from localhost (local API)
36
+ router.post('/', localhostOnly, parseRequest(MapShareCreateRequest), async (request) => {
37
+ const { mapId, receiverDeviceId } = request.parsed;
38
+ const mapInfo = await ctx.getMapInfo(mapId);
39
+ const mapShare = new MapShare({
40
+ ...mapInfo,
41
+ receiverDeviceId,
42
+ baseUrls: getRemoteBaseUrls(request.url, await ctx.getRemotePort()),
43
+ });
44
+ mapShares.set(mapShare.shareId, mapShare);
45
+ return Response.json(mapShare.state, {
46
+ status: 201,
47
+ headers: {
48
+ Location: new URL(mapShare.shareId, addTrailingSlash(request.url))
49
+ .href,
50
+ },
51
+ });
52
+ });
53
+ router.get('/', localhostOnly, () => {
54
+ return Array.from(mapShares.values()).map((ms) => ms.state);
55
+ });
56
+ router.get('/:shareId/events', localhostOnly, async (request) => {
57
+ const mapShare = getMapShare(request.params.shareId);
58
+ return createEventStreamResponse(mapShare, { signal: request.signal });
59
+ });
60
+ router.post('/:shareId/cancel', localhostOnly, async (request) => {
61
+ const mapShare = getMapShare(request.params.shareId);
62
+ mapShare.cancel();
63
+ return new Response(null, { status: 204 });
64
+ });
65
+ // These routes can be accessed by a remote peer, but the peer deviceId must
66
+ // match the receiverDeviceId on the map share
67
+ const validateRemoteDeviceId = async (request, { remoteDeviceId, isLocalhost }) => {
68
+ if (isLocalhost)
69
+ return;
70
+ if (!remoteDeviceId) {
71
+ throw new errors.FORBIDDEN();
72
+ }
73
+ const mapShare = getMapShare(request.params.shareId);
74
+ if (!timingSafeEqual(remoteDeviceId, mapShare.state.receiverDeviceId)) {
75
+ throw new errors.FORBIDDEN();
76
+ }
77
+ };
78
+ router.all('/:shareId', validateRemoteDeviceId);
79
+ router.all('/:shareId/*', validateRemoteDeviceId);
80
+ router.get('/:shareId', async (request) => {
81
+ return getMapShare(request.params.shareId).state;
82
+ });
83
+ router.get('/:shareId/download', async (request) => {
84
+ const mapShare = getMapShare(request.params.shareId);
85
+ const stream = ctx.createMapReadableStream(mapShare.state.mapId);
86
+ return mapShare.downloadResponse(stream);
87
+ });
88
+ const localDeclineHandler = async (request) => {
89
+ let parsedBody;
90
+ try {
91
+ const json = await request.json();
92
+ if (!CompiledLocalMapShareDeclineRequest.Check(json)) {
93
+ throw new errors.INVALID_REQUEST();
94
+ }
95
+ parsedBody = json;
96
+ }
97
+ catch (err) {
98
+ if ('status' in err)
99
+ throw err;
100
+ throw new errors.INVALID_REQUEST();
101
+ }
102
+ const { senderDeviceId, mapShareUrls, reason } = parsedBody;
103
+ const remotePublicKey = z32.decode(senderDeviceId);
104
+ const keyPair = ctx.getKeyPair();
105
+ let response;
106
+ // The sharer could have multiple IPs for different network interfaces, and
107
+ // not all of them may be on the same network as us, so try each URL until
108
+ // one works
109
+ for (const mapShareUrl of mapShareUrls) {
110
+ const url = new URL('decline', addTrailingSlash(mapShareUrl));
111
+ try {
112
+ response = (await secretStreamFetch(url, {
113
+ method: 'POST',
114
+ body: JSON.stringify({ reason }),
115
+ signal: request.signal,
116
+ dispatcher: new SecretStreamAgent({ remotePublicKey, keyPair }),
117
+ })); // Subtle difference bewteen Undici fetch Response and whatwg Response
118
+ break; // Exit loop on successful fetch
119
+ }
120
+ catch (error) {
121
+ if (error instanceof DOMException && error.name === 'AbortError') {
122
+ throw error; // Handle abort in caller
123
+ }
124
+ // Otherwise, try the next URL
125
+ }
126
+ }
127
+ if (!response) {
128
+ throw new errors.DECLINE_CANNOT_CONNECT();
129
+ }
130
+ if (!response.ok) {
131
+ // pass through error from sender
132
+ throw new StatusError(response.status, await response.json());
133
+ }
134
+ return new Response(null, { status: 204 });
135
+ };
136
+ const remoteDeclineHandler = async (request) => {
137
+ let parsedBody;
138
+ try {
139
+ const json = await request.json();
140
+ if (!CompiledRemoteMapShareDeclineRequest.Check(json)) {
141
+ throw new errors.INVALID_REQUEST();
142
+ }
143
+ parsedBody = json;
144
+ }
145
+ catch (err) {
146
+ if ('status' in err)
147
+ throw err;
148
+ throw new errors.INVALID_REQUEST();
149
+ }
150
+ const { reason } = parsedBody;
151
+ const mapShare = getMapShare(request.params.shareId);
152
+ mapShare.decline(reason);
153
+ return new Response(null, { status: 204 });
154
+ };
155
+ router.post('/:shareId/decline', async (request, { isLocalhost }) => {
156
+ if (isLocalhost) {
157
+ return localDeclineHandler(request);
158
+ }
159
+ else {
160
+ return remoteDeclineHandler(request);
161
+ }
162
+ });
163
+ return router;
164
+ function getMapShare(shareId) {
165
+ const mapShare = mapShares.get(shareId);
166
+ if (!mapShare) {
167
+ throw new errors.MAP_SHARE_NOT_FOUND(`Map share ID not found: ${shareId}`);
168
+ }
169
+ return mapShare;
170
+ }
171
+ }
172
+ /**
173
+ * Get the base URLs for downloads for all non-internal IPv4 addresses of the machine
174
+ */
175
+ function getRemoteBaseUrls(requestUrl, remotePort) {
176
+ requestUrl = addTrailingSlash(requestUrl);
177
+ const interfaces = os.networkInterfaces();
178
+ const baseUrls = [];
179
+ for (const iface of Object.values(interfaces)) {
180
+ if (!iface)
181
+ continue;
182
+ for (const addr of iface) {
183
+ if (addr.family === 'IPv4' && !addr.internal) {
184
+ const url = new URL(requestUrl);
185
+ url.hostname = addr.address;
186
+ url.port = remotePort.toString();
187
+ baseUrls.push(url.toString());
188
+ }
189
+ }
190
+ }
191
+ return baseUrls;
192
+ }
@@ -0,0 +1,6 @@
1
+ import { IRequestStrict } from 'itty-router';
2
+ import type { Context } from '../context.js';
3
+ export declare function MapsRouter({ base }: {
4
+ base?: string | undefined;
5
+ }, ctx: Context): import("itty-router").IttyRouterType<IRequestStrict, any[], any>;
6
+ //# sourceMappingURL=maps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maps.d.ts","sourceRoot":"","sources":["../../src/routes/maps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAmC,MAAM,aAAa,CAAA;AAI7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAe5C,wBAAgB,UAAU,CAAC,EAAE,IAAU,EAAE;;CAAA,EAAE,GAAG,EAAE,OAAO,oEA8HtD"}
@@ -0,0 +1,118 @@
1
+ import { IttyRouter } from 'itty-router';
2
+ import Mutex from 'p-mutex';
3
+ import { createServer as createSmpServer } from 'styled-map-package/server';
4
+ import { CUSTOM_MAP_ID, DEFAULT_MAP_ID, FALLBACK_MAP_ID, } from '../lib/constants.js';
5
+ import { errors } from '../lib/errors.js';
6
+ import { addTrailingSlash, noop } from '../lib/utils.js';
7
+ export function MapsRouter({ base = '/' }, ctx) {
8
+ base = addTrailingSlash(base);
9
+ const uploadMutexes = new Map();
10
+ const smpServer = createSmpServer({
11
+ base: `${base}:mapId/`,
12
+ });
13
+ const router = IttyRouter({ base });
14
+ router.get(`/:mapId/info`, async (request) => {
15
+ const info = await ctx.getMapInfo(request.params.mapId);
16
+ return {
17
+ created: info.mapCreated,
18
+ size: info.estimatedSizeBytes,
19
+ name: info.mapName,
20
+ };
21
+ });
22
+ const uploadHandler = async (request) => {
23
+ const writable = ctx.createMapWritableStream(request.params.mapId);
24
+ if (!request.body) {
25
+ throw new errors.INVALID_REQUEST('Request body is required');
26
+ }
27
+ await request.body.pipeTo(writable);
28
+ };
29
+ router.put('/:mapId', async (request) => {
30
+ // Only allow uploading to the custom map ID for now
31
+ if (request.params.mapId === DEFAULT_MAP_ID ||
32
+ request.params.mapId === FALLBACK_MAP_ID) {
33
+ throw new errors.FORBIDDEN(`Uploading to map ID "${request.params.mapId}" is not allowed`);
34
+ }
35
+ else if (request.params.mapId !== CUSTOM_MAP_ID) {
36
+ throw new errors.MAP_NOT_FOUND(`Map not found: ${request.params.mapId}`);
37
+ }
38
+ if (!request.body) {
39
+ throw new errors.INVALID_REQUEST('Request body is required');
40
+ }
41
+ // Get or create a mutex for this mapId to ensure sequential uploads
42
+ let mutex = uploadMutexes.get(request.params.mapId);
43
+ if (!mutex) {
44
+ mutex = new Mutex();
45
+ uploadMutexes.set(request.params.mapId, mutex);
46
+ }
47
+ await mutex.withLock(() => uploadHandler(request));
48
+ return new Response(null, { status: 200 });
49
+ });
50
+ router.delete('/:mapId', async (request) => {
51
+ // Only allow deleting the custom map ID
52
+ if (request.params.mapId === DEFAULT_MAP_ID ||
53
+ request.params.mapId === FALLBACK_MAP_ID) {
54
+ throw new errors.FORBIDDEN(`Deleting the 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
+ // Use mutex to wait for any active uploads to complete before deleting
60
+ let mutex = uploadMutexes.get(request.params.mapId);
61
+ if (!mutex) {
62
+ mutex = new Mutex();
63
+ uploadMutexes.set(request.params.mapId, mutex);
64
+ }
65
+ await mutex.withLock(() => ctx.deleteMap(request.params.mapId));
66
+ return new Response(null, { status: 204 });
67
+ });
68
+ router.all(`/:mapId/*`, async (request) => {
69
+ if (request.params.mapId === DEFAULT_MAP_ID) {
70
+ return defaultMapHandler(request);
71
+ }
72
+ // Get the reader first - this throws MAP_NOT_FOUND for unknown map IDs
73
+ const reader = await ctx.getReader(request.params.mapId);
74
+ try {
75
+ return await smpServer.fetch(request, reader);
76
+ }
77
+ catch (err) {
78
+ // Convert generic 404 from smpServer to RESOURCE_NOT_FOUND
79
+ if (err instanceof Error && 'status' in err && err.status === 404) {
80
+ throw new errors.RESOURCE_NOT_FOUND();
81
+ }
82
+ throw err;
83
+ }
84
+ });
85
+ // Special handler for the default map ID that tries to serve a custom map
86
+ // if available, otherwise falls back to the online style or bundled fallback
87
+ const defaultMapHandler = async (request) => {
88
+ const defaultOnlineStyleUrl = ctx.getDefaultOnlineStyleUrl();
89
+ const styleUrls = [
90
+ new URL(`../${CUSTOM_MAP_ID}/style.json`, request.url),
91
+ defaultOnlineStyleUrl,
92
+ new URL(`../${FALLBACK_MAP_ID}/style.json`, request.url),
93
+ ];
94
+ for (const url of styleUrls) {
95
+ let response;
96
+ if (url === defaultOnlineStyleUrl) {
97
+ response = await fetch(url).catch(noop);
98
+ }
99
+ else {
100
+ // No need to go through the networking stack for local requests
101
+ response = await router.fetch(new Request(url)).catch(noop);
102
+ }
103
+ response?.body?.cancel(); // Close the connection
104
+ if (response && response.ok) {
105
+ return new Response(null, {
106
+ status: 302,
107
+ headers: {
108
+ location: url.toString(),
109
+ 'access-control-allow-origin': '*',
110
+ 'cache-control': 'no-cache',
111
+ },
112
+ });
113
+ }
114
+ }
115
+ throw new errors.MAP_NOT_FOUND('No available map style found');
116
+ };
117
+ return router;
118
+ }
@@ -0,0 +1,6 @@
1
+ import type { Context } from '../context.js';
2
+ import type { RouterExternal } from '../types.js';
3
+ export declare function RootRouter({ base }: {
4
+ base?: string | undefined;
5
+ }, ctx: Context): RouterExternal;
6
+ //# sourceMappingURL=root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../../src/routes/root.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAG5C,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAA;AAS/D,wBAAgB,UAAU,CAAC,EAAE,IAAU,EAAE;;CAAA,EAAE,GAAG,EAAE,OAAO,GAAG,cAAc,CAsBvE"}
@@ -0,0 +1,29 @@
1
+ import { json, Router } from 'itty-router';
2
+ import { error } from '../lib/errors.js';
3
+ import { localhostOnly } from '../middlewares/localhost-only.js';
4
+ import { DownloadsRouter } from './downloads.js';
5
+ import { MapSharesRouter } from './map-shares.js';
6
+ import { MapsRouter } from './maps.js';
7
+ const MAPS_BASE = '/maps/';
8
+ const MAP_SHARES_BASE = '/mapShares/';
9
+ const DOWNLOADS_BASE = '/downloads/';
10
+ export function RootRouter({ base = '/' }, ctx) {
11
+ const router = Router({
12
+ base,
13
+ // The `error` handler will send a response with the status code from any
14
+ // thrown StatusError, or a 500 for any other errors.
15
+ catch: (err) => error(err),
16
+ // Sends a 404 response for any requests that don't match a route, and for
17
+ // any request handlers that return JSON will send a JSON response.
18
+ finally: [(response) => response ?? error(404), json],
19
+ });
20
+ const mapsRouter = MapsRouter({ base: MAPS_BASE }, ctx);
21
+ const downloadsRouter = DownloadsRouter({ base: DOWNLOADS_BASE }, ctx);
22
+ const mapSharesRouter = MapSharesRouter({ base: MAP_SHARES_BASE }, ctx);
23
+ router.all(`${MAPS_BASE}*`, localhostOnly, mapsRouter.fetch);
24
+ router.all(`${DOWNLOADS_BASE}*`, localhostOnly, downloadsRouter.fetch);
25
+ // Some map share routes are remote-accessible - localhostOnly is applied in
26
+ // the map shares router where needed
27
+ router.all(`${MAP_SHARES_BASE}*`, mapSharesRouter.fetch);
28
+ return router;
29
+ }