@scalemule/news-publication 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -111,3 +111,45 @@ The review endpoint returns the decision for the exact invited revision. Editors
111
111
  ### Publication framing for private previews (0.0.28)
112
112
 
113
113
  Pass `publicationHeader` and `publicationFooter` React nodes to `StoryReview` to reuse a publication's real navigation, masthead, and footer. The private banner remains above the publication header and the review controls remain separate from the story. The standalone masthead remains the default. The host must suppress analytics on private review routes; public article indexing and sharing features are not mounted by these slots. This patch changes presentation only; review permissions and publication gates are unchanged.
114
+
115
+ ### Private review sign-in and email options
116
+
117
+ `StoryReview` accepts `readerAccountUrl` and a client `renderSignIn(returnTo)`
118
+ function so the host publication can reuse its existing account and sign-in
119
+ components. Hosts must implement the `/api/news/review/:session/account`
120
+ server adapter to connect the authenticated, verified reader through Blog’s
121
+ `join-account` endpoint. Never trust a browser-supplied email or verification flag.
122
+ The private return path belongs in the host’s signed, HttpOnly OAuth flow cookie,
123
+ not the provider’s authorization URL. Contributor access does not imply subject
124
+ approval authority.
125
+
126
+ Optional questions can be browsed and copied before identity verification.
127
+ `feedbackEmail` enables mail-app links containing the story title, exact revision
128
+ reference and optional answers, never the invitation credential. Configure a
129
+ monitored mailbox; received email must be recorded by the newsroom. Online
130
+ answers retain their existing autosave and explicit-send behavior.
131
+
132
+ ## Community Maps
133
+
134
+ The `community-maps-server` subpath exports `createPublicationCommunityMapsHandler`.
135
+ Supply a resolver that verifies the publication host using your existing site lookup,
136
+ then returns its server API key, gateway origin, canonical public origin, and trusted
137
+ reader context. Forward only the public allowlisted catch-all route. The handler
138
+ checks POST origins, bounds uploads, signs an HttpOnly reader cookie, and applies
139
+ no-store responses. It never proxies editorial routes or accepts a caller-selected
140
+ application ID. Use a Node server runtime; this module imports node:crypto.
141
+
142
+ The `community-maps-view` subpath exports `PublicationCommunityMapPage` and
143
+ `CommunityMapArticleEmbeds`. Supply the publication chrome and import the React map CSS.
144
+ The optional React peer is required only when consuming these views. Use the map
145
+ worker asset CLI from `@scalemule/react` before building the site.
146
+
147
+ Article integration uses the existing Blog metadata extension:
148
+
149
+ ```json
150
+ {"community_maps":[{"collection_slug":"halloween-2026","mode":"map"}]}
151
+ ```
152
+
153
+ Other modes are `cards` and `features`; optional `area_id` / `feature_ids` select live
154
+ records. No copied feature snapshot is embedded in a story, so withdrawal takes effect
155
+ on the next read. Publishers should apply their caption policy to public media copy.
@@ -0,0 +1,23 @@
1
+ // src/community-maps.ts
2
+ function communityMapEmbeds(metadata) {
3
+ if (!metadata || typeof metadata !== "object") return [];
4
+ const value = metadata.community_maps;
5
+ if (!Array.isArray(value)) return [];
6
+ return value.slice(0, 5).flatMap((entry) => {
7
+ if (!entry || typeof entry !== "object" || typeof entry.collection_slug !== "string" || !/^[a-z0-9-]{1,191}$/.test(entry.collection_slug) || !["map", "cards", "features"].includes(entry.mode))
8
+ return [];
9
+ const ids = Array.isArray(entry.feature_ids) ? entry.feature_ids.filter(
10
+ (id) => typeof id === "string" && /^[0-9a-f-]{36}$/i.test(id)
11
+ ).slice(0, 20) : void 0;
12
+ return [
13
+ {
14
+ collection_slug: entry.collection_slug,
15
+ mode: entry.mode,
16
+ feature_ids: ids,
17
+ area_id: typeof entry.area_id === "string" && /^[0-9a-f-]{36}$/i.test(entry.area_id) ? entry.area_id : void 0
18
+ }
19
+ ];
20
+ });
21
+ }
22
+
23
+ export { communityMapEmbeds };
@@ -0,0 +1,10 @@
1
+ interface CommunityMapArticleEmbed {
2
+ collection_slug: string;
3
+ feature_ids?: string[];
4
+ area_id?: string;
5
+ mode: "map" | "cards" | "features";
6
+ }
7
+ /** Article metadata extension: references only; records remain in Community Maps. */
8
+ declare function communityMapEmbeds(metadata: unknown): CommunityMapArticleEmbed[];
9
+
10
+ export { type CommunityMapArticleEmbed as C, communityMapEmbeds as c };
@@ -0,0 +1,10 @@
1
+ interface CommunityMapArticleEmbed {
2
+ collection_slug: string;
3
+ feature_ids?: string[];
4
+ area_id?: string;
5
+ mode: "map" | "cards" | "features";
6
+ }
7
+ /** Article metadata extension: references only; records remain in Community Maps. */
8
+ declare function communityMapEmbeds(metadata: unknown): CommunityMapArticleEmbed[];
9
+
10
+ export { type CommunityMapArticleEmbed as C, communityMapEmbeds as c };
@@ -0,0 +1,250 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var sdk = require('@scalemule/sdk');
5
+
6
+ // src/community-maps-server.ts
7
+ var ROOT = "/v1/knowledge/community-maps";
8
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
+ var COOKIE = "sm_community_reader";
10
+ var json = (data, status = 200) => Response.json(data, {
11
+ status,
12
+ headers: {
13
+ "cache-control": "private, no-store",
14
+ "x-content-type-options": "nosniff"
15
+ }
16
+ });
17
+ function identity(request, key, origin) {
18
+ const sign = (id2) => crypto.createHmac("sha256", key).update("community-maps-reader:" + id2).digest("base64url");
19
+ const raw = request.headers.get("cookie")?.split(";").map((v) => v.trim()).find((v) => v.startsWith(COOKIE + "="))?.slice(COOKIE.length + 1);
20
+ if (raw) {
21
+ const [id2, signature] = raw.split(".");
22
+ if (id2 && signature && UUID.test(id2) && /^[A-Za-z0-9_-]{43}$/.test(signature)) {
23
+ const expected = sign(id2);
24
+ if (expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)))
25
+ return { id: id2, cookie: null };
26
+ }
27
+ }
28
+ const id = crypto.randomUUID();
29
+ return {
30
+ id,
31
+ cookie: `${COOKIE}=${id}.${sign(id)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000${new URL(origin).protocol === "https:" ? "; Secure" : ""}`
32
+ };
33
+ }
34
+ async function boundedBody(request, max) {
35
+ if (Number(request.headers.get("content-length") || 0) > max)
36
+ throw new ProxyProblem(413, "The submission is too large.");
37
+ const reader = request.body?.getReader();
38
+ if (!reader) return new Uint8Array();
39
+ const chunks = [];
40
+ let length = 0;
41
+ try {
42
+ while (true) {
43
+ const { value, done } = await reader.read();
44
+ if (done) break;
45
+ length += value.length;
46
+ if (length > max) {
47
+ await reader.cancel();
48
+ throw new ProxyProblem(413, "The submission is too large.");
49
+ }
50
+ chunks.push(value);
51
+ }
52
+ } finally {
53
+ reader.releaseLock();
54
+ }
55
+ return Buffer.concat(chunks);
56
+ }
57
+ var ProxyProblem = class extends Error {
58
+ constructor(status, message) {
59
+ super(message);
60
+ this.status = status;
61
+ }
62
+ status;
63
+ };
64
+ function createPublicationCommunityMapsHandler(options) {
65
+ return async (request, path) => {
66
+ try {
67
+ if (path[0] !== "collections" || !path[1] || !/^[a-z0-9-]{1,191}$/.test(path[1]))
68
+ return json({ error: { message: "Guide not found" } }, 404);
69
+ const slug = path[1];
70
+ const rest = path.slice(2);
71
+ const method = request.method;
72
+ const read = method === "GET" && (rest.length === 0 || rest[0] === "features" && (rest.length === 1 || rest.length === 2 && UUID.test(rest[1])) || rest[0] === "media" && rest.length === 2 && UUID.test(rest[1]));
73
+ const upload = method === "POST" && rest.length === 3 && rest[0] === "submissions" && UUID.test(rest[1]) && rest[2] === "media";
74
+ const submission = method === "POST" && rest.length === 1 && rest[0] === "submissions";
75
+ const action = method === "POST" && rest.length === 3 && rest[0] === "features" && UUID.test(rest[1]) && ["reactions", "reports"].includes(rest[2]);
76
+ if (!read && !upload && !submission && !action)
77
+ return json({ error: { message: "Route not found" } }, 404);
78
+ const scope = await options.resolve(request);
79
+ if (method !== "GET" && request.headers.get("origin") !== new URL(scope.origin).origin)
80
+ return json(
81
+ { error: { message: "Submit from this publication\u2019s website." } },
82
+ 403
83
+ );
84
+ const gateway = new URL(scope.gatewayUrl);
85
+ if (gateway.protocol !== "https:" && !(gateway.protocol === "http:" && ["127.0.0.1", "localhost"].includes(gateway.hostname)))
86
+ throw new ProxyProblem(503, "The guide is unavailable.");
87
+ if (!scope.apiKey)
88
+ throw new ProxyProblem(503, "The guide is unavailable.");
89
+ const reader = identity(request, scope.apiKey, scope.origin);
90
+ const clientContext = { ...scope.clientContext, anonymousId: reader.id };
91
+ const sdk$1 = new sdk.ScaleMule({
92
+ apiKey: scope.apiKey,
93
+ baseUrl: scope.gatewayUrl
94
+ });
95
+ const callOptions = {
96
+ clientContext,
97
+ headers: scope.accessToken ? { authorization: `Bearer ${scope.accessToken}` } : {}
98
+ };
99
+ let response;
100
+ if (read && rest[0] === "media") {
101
+ const upstream = await fetch(
102
+ `${gateway.origin}${ROOT}/collections/${slug}/media/${rest[1]}`,
103
+ {
104
+ headers: {
105
+ ...sdk.buildClientContextHeaders(clientContext),
106
+ "x-api-key": scope.apiKey,
107
+ ...callOptions.headers
108
+ },
109
+ cache: "no-store",
110
+ redirect: "error",
111
+ signal: AbortSignal.timeout(3e4)
112
+ }
113
+ );
114
+ response = upstream.ok ? new Response(upstream.body, {
115
+ headers: {
116
+ "content-type": "image/png",
117
+ "cache-control": "private, no-store",
118
+ "x-content-type-options": "nosniff"
119
+ }
120
+ }) : json(
121
+ { error: { message: "Photo unavailable" } },
122
+ upstream.status >= 500 ? 503 : upstream.status
123
+ );
124
+ } else {
125
+ let result;
126
+ if (read) {
127
+ if (!rest.length)
128
+ result = await sdk$1.communityMaps.getCollection(slug, callOptions);
129
+ else if (rest.length === 2)
130
+ result = await sdk$1.communityMaps.getFeature(
131
+ slug,
132
+ rest[1],
133
+ callOptions
134
+ );
135
+ else {
136
+ const query = {};
137
+ const params = new URL(request.url).searchParams;
138
+ for (const key of [
139
+ "after",
140
+ "area_id",
141
+ "category",
142
+ "geometry_type",
143
+ "active_at",
144
+ "bbox"
145
+ ]) {
146
+ const value = params.get(key);
147
+ if (value) query[key] = value;
148
+ }
149
+ const limit = params.get("limit");
150
+ if (limit)
151
+ query.limit = Math.min(200, Math.max(1, Number(limit) || 50));
152
+ const filters = { ...query };
153
+ for (const key of ["featured", "verified"]) {
154
+ const value = params.get(key);
155
+ if (value === "true" || value === "false")
156
+ filters[key] = value === "true";
157
+ }
158
+ result = await sdk$1.communityMaps.listFeatures(
159
+ slug,
160
+ filters,
161
+ callOptions
162
+ );
163
+ }
164
+ } else if (upload) {
165
+ const contentType = request.headers.get("content-type") || "";
166
+ if (!contentType.startsWith("multipart/form-data;"))
167
+ throw new ProxyProblem(415, "Choose a photograph to upload.");
168
+ const bytes = await boundedBody(request, 9 * 1024 * 1024);
169
+ const form = await new Response(bytes, {
170
+ headers: { "content-type": contentType }
171
+ }).formData();
172
+ const file = form.get("file");
173
+ if (!(file instanceof Blob))
174
+ throw new ProxyProblem(400, "Photo is required.");
175
+ result = await sdk$1.communityMaps.uploadPhoto(
176
+ slug,
177
+ rest[1],
178
+ file,
179
+ {
180
+ caption: String(form.get("caption") || ""),
181
+ attribution: String(form.get("attribution") || ""),
182
+ rights_confirmed: form.get("rights_confirmed") === "true"
183
+ },
184
+ callOptions
185
+ );
186
+ } else {
187
+ if (!request.headers.get("content-type")?.startsWith("application/json"))
188
+ throw new ProxyProblem(415, "Use the community submission form.");
189
+ let body;
190
+ try {
191
+ body = JSON.parse(
192
+ new TextDecoder().decode(await boundedBody(request, 65536))
193
+ );
194
+ } catch (e) {
195
+ if (e instanceof ProxyProblem) throw e;
196
+ throw new ProxyProblem(400, "Invalid submission.");
197
+ }
198
+ if (!body || typeof body !== "object" || Array.isArray(body))
199
+ throw new ProxyProblem(400, "Invalid submission.");
200
+ if (submission)
201
+ result = await sdk$1.communityMaps.createSubmission(
202
+ slug,
203
+ body,
204
+ callOptions
205
+ );
206
+ else if (rest[2] === "reactions")
207
+ result = await sdk$1.communityMaps.react(
208
+ slug,
209
+ rest[1],
210
+ body.reaction,
211
+ body.active !== false,
212
+ callOptions
213
+ );
214
+ else
215
+ result = await sdk$1.communityMaps.report(
216
+ slug,
217
+ rest[1],
218
+ body,
219
+ callOptions
220
+ );
221
+ }
222
+ if (result.error) {
223
+ const error = result.error;
224
+ const status = typeof error.status === "number" ? error.status : 400;
225
+ response = json(
226
+ {
227
+ error: {
228
+ message: status >= 500 ? "The guide is temporarily unavailable. Please try again." : error.message
229
+ }
230
+ },
231
+ status
232
+ );
233
+ } else response = json({ data: result.data });
234
+ }
235
+ if (reader.cookie) response.headers.set("set-cookie", reader.cookie);
236
+ return response;
237
+ } catch (error) {
238
+ return json(
239
+ {
240
+ error: {
241
+ message: error instanceof ProxyProblem ? error.message : "The guide is temporarily unavailable. Please try again."
242
+ }
243
+ },
244
+ error instanceof ProxyProblem ? error.status : 503
245
+ );
246
+ }
247
+ };
248
+ }
249
+
250
+ exports.createPublicationCommunityMapsHandler = createPublicationCommunityMapsHandler;
@@ -0,0 +1,16 @@
1
+ import { ClientContext } from '@scalemule/sdk';
2
+
3
+ interface PublicationCommunityMapScope {
4
+ apiKey: string;
5
+ gatewayUrl: string;
6
+ origin: string;
7
+ clientContext: ClientContext;
8
+ accessToken?: string;
9
+ }
10
+ interface PublicationCommunityMapServerOptions {
11
+ resolve: (request: Request) => Promise<PublicationCommunityMapScope>;
12
+ }
13
+ /** Only public collection routes are accepted. The URL never selects tenant credentials. */
14
+ declare function createPublicationCommunityMapsHandler(options: PublicationCommunityMapServerOptions): (request: Request, path: string[]) => Promise<Response>;
15
+
16
+ export { type PublicationCommunityMapScope, type PublicationCommunityMapServerOptions, createPublicationCommunityMapsHandler };
@@ -0,0 +1,16 @@
1
+ import { ClientContext } from '@scalemule/sdk';
2
+
3
+ interface PublicationCommunityMapScope {
4
+ apiKey: string;
5
+ gatewayUrl: string;
6
+ origin: string;
7
+ clientContext: ClientContext;
8
+ accessToken?: string;
9
+ }
10
+ interface PublicationCommunityMapServerOptions {
11
+ resolve: (request: Request) => Promise<PublicationCommunityMapScope>;
12
+ }
13
+ /** Only public collection routes are accepted. The URL never selects tenant credentials. */
14
+ declare function createPublicationCommunityMapsHandler(options: PublicationCommunityMapServerOptions): (request: Request, path: string[]) => Promise<Response>;
15
+
16
+ export { type PublicationCommunityMapScope, type PublicationCommunityMapServerOptions, createPublicationCommunityMapsHandler };
@@ -0,0 +1,248 @@
1
+ import { timingSafeEqual, randomUUID, createHmac } from 'crypto';
2
+ import { ScaleMule, buildClientContextHeaders } from '@scalemule/sdk';
3
+
4
+ // src/community-maps-server.ts
5
+ var ROOT = "/v1/knowledge/community-maps";
6
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+ var COOKIE = "sm_community_reader";
8
+ var json = (data, status = 200) => Response.json(data, {
9
+ status,
10
+ headers: {
11
+ "cache-control": "private, no-store",
12
+ "x-content-type-options": "nosniff"
13
+ }
14
+ });
15
+ function identity(request, key, origin) {
16
+ const sign = (id2) => createHmac("sha256", key).update("community-maps-reader:" + id2).digest("base64url");
17
+ const raw = request.headers.get("cookie")?.split(";").map((v) => v.trim()).find((v) => v.startsWith(COOKIE + "="))?.slice(COOKIE.length + 1);
18
+ if (raw) {
19
+ const [id2, signature] = raw.split(".");
20
+ if (id2 && signature && UUID.test(id2) && /^[A-Za-z0-9_-]{43}$/.test(signature)) {
21
+ const expected = sign(id2);
22
+ if (expected.length === signature.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected)))
23
+ return { id: id2, cookie: null };
24
+ }
25
+ }
26
+ const id = randomUUID();
27
+ return {
28
+ id,
29
+ cookie: `${COOKIE}=${id}.${sign(id)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000${new URL(origin).protocol === "https:" ? "; Secure" : ""}`
30
+ };
31
+ }
32
+ async function boundedBody(request, max) {
33
+ if (Number(request.headers.get("content-length") || 0) > max)
34
+ throw new ProxyProblem(413, "The submission is too large.");
35
+ const reader = request.body?.getReader();
36
+ if (!reader) return new Uint8Array();
37
+ const chunks = [];
38
+ let length = 0;
39
+ try {
40
+ while (true) {
41
+ const { value, done } = await reader.read();
42
+ if (done) break;
43
+ length += value.length;
44
+ if (length > max) {
45
+ await reader.cancel();
46
+ throw new ProxyProblem(413, "The submission is too large.");
47
+ }
48
+ chunks.push(value);
49
+ }
50
+ } finally {
51
+ reader.releaseLock();
52
+ }
53
+ return Buffer.concat(chunks);
54
+ }
55
+ var ProxyProblem = class extends Error {
56
+ constructor(status, message) {
57
+ super(message);
58
+ this.status = status;
59
+ }
60
+ status;
61
+ };
62
+ function createPublicationCommunityMapsHandler(options) {
63
+ return async (request, path) => {
64
+ try {
65
+ if (path[0] !== "collections" || !path[1] || !/^[a-z0-9-]{1,191}$/.test(path[1]))
66
+ return json({ error: { message: "Guide not found" } }, 404);
67
+ const slug = path[1];
68
+ const rest = path.slice(2);
69
+ const method = request.method;
70
+ const read = method === "GET" && (rest.length === 0 || rest[0] === "features" && (rest.length === 1 || rest.length === 2 && UUID.test(rest[1])) || rest[0] === "media" && rest.length === 2 && UUID.test(rest[1]));
71
+ const upload = method === "POST" && rest.length === 3 && rest[0] === "submissions" && UUID.test(rest[1]) && rest[2] === "media";
72
+ const submission = method === "POST" && rest.length === 1 && rest[0] === "submissions";
73
+ const action = method === "POST" && rest.length === 3 && rest[0] === "features" && UUID.test(rest[1]) && ["reactions", "reports"].includes(rest[2]);
74
+ if (!read && !upload && !submission && !action)
75
+ return json({ error: { message: "Route not found" } }, 404);
76
+ const scope = await options.resolve(request);
77
+ if (method !== "GET" && request.headers.get("origin") !== new URL(scope.origin).origin)
78
+ return json(
79
+ { error: { message: "Submit from this publication\u2019s website." } },
80
+ 403
81
+ );
82
+ const gateway = new URL(scope.gatewayUrl);
83
+ if (gateway.protocol !== "https:" && !(gateway.protocol === "http:" && ["127.0.0.1", "localhost"].includes(gateway.hostname)))
84
+ throw new ProxyProblem(503, "The guide is unavailable.");
85
+ if (!scope.apiKey)
86
+ throw new ProxyProblem(503, "The guide is unavailable.");
87
+ const reader = identity(request, scope.apiKey, scope.origin);
88
+ const clientContext = { ...scope.clientContext, anonymousId: reader.id };
89
+ const sdk = new ScaleMule({
90
+ apiKey: scope.apiKey,
91
+ baseUrl: scope.gatewayUrl
92
+ });
93
+ const callOptions = {
94
+ clientContext,
95
+ headers: scope.accessToken ? { authorization: `Bearer ${scope.accessToken}` } : {}
96
+ };
97
+ let response;
98
+ if (read && rest[0] === "media") {
99
+ const upstream = await fetch(
100
+ `${gateway.origin}${ROOT}/collections/${slug}/media/${rest[1]}`,
101
+ {
102
+ headers: {
103
+ ...buildClientContextHeaders(clientContext),
104
+ "x-api-key": scope.apiKey,
105
+ ...callOptions.headers
106
+ },
107
+ cache: "no-store",
108
+ redirect: "error",
109
+ signal: AbortSignal.timeout(3e4)
110
+ }
111
+ );
112
+ response = upstream.ok ? new Response(upstream.body, {
113
+ headers: {
114
+ "content-type": "image/png",
115
+ "cache-control": "private, no-store",
116
+ "x-content-type-options": "nosniff"
117
+ }
118
+ }) : json(
119
+ { error: { message: "Photo unavailable" } },
120
+ upstream.status >= 500 ? 503 : upstream.status
121
+ );
122
+ } else {
123
+ let result;
124
+ if (read) {
125
+ if (!rest.length)
126
+ result = await sdk.communityMaps.getCollection(slug, callOptions);
127
+ else if (rest.length === 2)
128
+ result = await sdk.communityMaps.getFeature(
129
+ slug,
130
+ rest[1],
131
+ callOptions
132
+ );
133
+ else {
134
+ const query = {};
135
+ const params = new URL(request.url).searchParams;
136
+ for (const key of [
137
+ "after",
138
+ "area_id",
139
+ "category",
140
+ "geometry_type",
141
+ "active_at",
142
+ "bbox"
143
+ ]) {
144
+ const value = params.get(key);
145
+ if (value) query[key] = value;
146
+ }
147
+ const limit = params.get("limit");
148
+ if (limit)
149
+ query.limit = Math.min(200, Math.max(1, Number(limit) || 50));
150
+ const filters = { ...query };
151
+ for (const key of ["featured", "verified"]) {
152
+ const value = params.get(key);
153
+ if (value === "true" || value === "false")
154
+ filters[key] = value === "true";
155
+ }
156
+ result = await sdk.communityMaps.listFeatures(
157
+ slug,
158
+ filters,
159
+ callOptions
160
+ );
161
+ }
162
+ } else if (upload) {
163
+ const contentType = request.headers.get("content-type") || "";
164
+ if (!contentType.startsWith("multipart/form-data;"))
165
+ throw new ProxyProblem(415, "Choose a photograph to upload.");
166
+ const bytes = await boundedBody(request, 9 * 1024 * 1024);
167
+ const form = await new Response(bytes, {
168
+ headers: { "content-type": contentType }
169
+ }).formData();
170
+ const file = form.get("file");
171
+ if (!(file instanceof Blob))
172
+ throw new ProxyProblem(400, "Photo is required.");
173
+ result = await sdk.communityMaps.uploadPhoto(
174
+ slug,
175
+ rest[1],
176
+ file,
177
+ {
178
+ caption: String(form.get("caption") || ""),
179
+ attribution: String(form.get("attribution") || ""),
180
+ rights_confirmed: form.get("rights_confirmed") === "true"
181
+ },
182
+ callOptions
183
+ );
184
+ } else {
185
+ if (!request.headers.get("content-type")?.startsWith("application/json"))
186
+ throw new ProxyProblem(415, "Use the community submission form.");
187
+ let body;
188
+ try {
189
+ body = JSON.parse(
190
+ new TextDecoder().decode(await boundedBody(request, 65536))
191
+ );
192
+ } catch (e) {
193
+ if (e instanceof ProxyProblem) throw e;
194
+ throw new ProxyProblem(400, "Invalid submission.");
195
+ }
196
+ if (!body || typeof body !== "object" || Array.isArray(body))
197
+ throw new ProxyProblem(400, "Invalid submission.");
198
+ if (submission)
199
+ result = await sdk.communityMaps.createSubmission(
200
+ slug,
201
+ body,
202
+ callOptions
203
+ );
204
+ else if (rest[2] === "reactions")
205
+ result = await sdk.communityMaps.react(
206
+ slug,
207
+ rest[1],
208
+ body.reaction,
209
+ body.active !== false,
210
+ callOptions
211
+ );
212
+ else
213
+ result = await sdk.communityMaps.report(
214
+ slug,
215
+ rest[1],
216
+ body,
217
+ callOptions
218
+ );
219
+ }
220
+ if (result.error) {
221
+ const error = result.error;
222
+ const status = typeof error.status === "number" ? error.status : 400;
223
+ response = json(
224
+ {
225
+ error: {
226
+ message: status >= 500 ? "The guide is temporarily unavailable. Please try again." : error.message
227
+ }
228
+ },
229
+ status
230
+ );
231
+ } else response = json({ data: result.data });
232
+ }
233
+ if (reader.cookie) response.headers.set("set-cookie", reader.cookie);
234
+ return response;
235
+ } catch (error) {
236
+ return json(
237
+ {
238
+ error: {
239
+ message: error instanceof ProxyProblem ? error.message : "The guide is temporarily unavailable. Please try again."
240
+ }
241
+ },
242
+ error instanceof ProxyProblem ? error.status : 503
243
+ );
244
+ }
245
+ };
246
+ }
247
+
248
+ export { createPublicationCommunityMapsHandler };