@scalemule/news-publication 0.0.48 → 0.0.50

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 (76) hide show
  1. package/README.md +54 -0
  2. package/dist/chunk-4JRWZXAP.js +62 -0
  3. package/dist/chunk-ABPJMDUG.js +251 -0
  4. package/dist/chunk-AFJ6S5WJ.js +124 -0
  5. package/dist/{chunk-GB36YM56.js → chunk-AMLVCZDP.js} +11 -1
  6. package/dist/chunk-QW3OZB22.js +616 -0
  7. package/dist/community-maps-view.d.cts +3 -3
  8. package/dist/community-maps-view.d.ts +3 -3
  9. package/dist/cookie-consent-banner.d.cts +2 -2
  10. package/dist/cookie-consent-banner.d.ts +2 -2
  11. package/dist/cookie-settings-view.d.cts +2 -2
  12. package/dist/cookie-settings-view.d.ts +2 -2
  13. package/dist/index.cjs +29 -1
  14. package/dist/index.d.cts +4 -3
  15. package/dist/index.d.ts +4 -3
  16. package/dist/index.js +3 -2
  17. package/dist/marketplace-forms.d.cts +3 -3
  18. package/dist/marketplace-forms.d.ts +3 -3
  19. package/dist/marketplace-view.d.cts +5 -5
  20. package/dist/marketplace-view.d.ts +5 -5
  21. package/dist/{meet-RWmfj3I3.d.cts → meet-BWpxY20X.d.ts} +5 -0
  22. package/dist/{meet-RWmfj3I3.d.ts → meet-CO9Unz5M.d.cts} +5 -0
  23. package/dist/meet-forms.d.cts +7 -6
  24. package/dist/meet-forms.d.ts +7 -6
  25. package/dist/meet-forms.js +2 -1
  26. package/dist/meet-view.cjs +22 -2
  27. package/dist/meet-view.d.cts +7 -5
  28. package/dist/meet-view.d.ts +7 -5
  29. package/dist/meet-view.js +21 -4
  30. package/dist/network-bar.d.cts +2 -2
  31. package/dist/network-bar.d.ts +2 -2
  32. package/dist/network-copyright.d.cts +2 -2
  33. package/dist/network-copyright.d.ts +2 -2
  34. package/dist/pets-auth-server.cjs +257 -0
  35. package/dist/pets-auth-server.d.cts +23 -0
  36. package/dist/pets-auth-server.d.ts +23 -0
  37. package/dist/pets-auth-server.js +198 -0
  38. package/dist/pets-forms.cjs +764 -0
  39. package/dist/pets-forms.d.cts +28 -0
  40. package/dist/pets-forms.d.ts +28 -0
  41. package/dist/pets-forms.js +714 -0
  42. package/dist/pets-interactions.cjs +333 -0
  43. package/dist/pets-interactions.d.cts +23 -0
  44. package/dist/pets-interactions.d.ts +23 -0
  45. package/dist/pets-interactions.js +320 -0
  46. package/dist/pets-plugin.cjs +546 -0
  47. package/dist/pets-plugin.d.cts +50 -0
  48. package/dist/pets-plugin.d.ts +50 -0
  49. package/dist/pets-plugin.js +312 -0
  50. package/dist/pets-publication.cjs +1014 -0
  51. package/dist/pets-publication.d.cts +40 -0
  52. package/dist/pets-publication.d.ts +40 -0
  53. package/dist/pets-publication.js +201 -0
  54. package/dist/pets-server.cjs +289 -0
  55. package/dist/pets-server.d.cts +42 -0
  56. package/dist/pets-server.d.ts +42 -0
  57. package/dist/pets-server.js +2 -0
  58. package/dist/pets-view.cjs +714 -0
  59. package/dist/pets-view.d.cts +58 -0
  60. package/dist/pets-view.d.ts +58 -0
  61. package/dist/pets-view.js +4 -0
  62. package/dist/pets.cjs +138 -0
  63. package/dist/pets.d.cts +127 -0
  64. package/dist/pets.d.ts +127 -0
  65. package/dist/pets.js +1 -0
  66. package/dist/sponsorship-view.d.cts +2 -2
  67. package/dist/sponsorship-view.d.ts +2 -2
  68. package/dist/sponsorship-view.js +2 -62
  69. package/dist/story-review.d.cts +6 -6
  70. package/dist/story-review.d.ts +6 -6
  71. package/dist/theme-toggle.d.cts +2 -2
  72. package/dist/theme-toggle.d.ts +2 -2
  73. package/dist/verified-card.d.cts +3 -3
  74. package/dist/verified-card.d.ts +3 -3
  75. package/package.json +45 -3
  76. package/pets.css +746 -0
package/README.md CHANGED
@@ -245,3 +245,57 @@ publication must validate that return path and preserve it through sign-in.
245
245
  Inventory is unavailable until a verified operational placement is active. `placementAvailable` enforces verification freshness and keeps paid briefings disabled. Campaign approval, authorization, allocation capacity and manual commercial references remain server responsibilities. Report HTML distinguishes observed counts from unavailable data and can be printed or rendered to PDF. No invoice or payment is initiated by this SDK.
246
246
 
247
247
  The consent banner now offers a choice in every geography where optional measurement requires one. `measurementConsent` is the server gate for current, explicitly allowed categories and GPC/DNT. Positive permission is local: unsigned URL parameters and a binary network hub cannot grant all categories. Rendering sponsorship recognition does not require tracking consent; measuring it requires both performance and advertising permission.
248
+
249
+ ## Community pet sections
250
+
251
+ The `pets`, `pets-view`, `pets-forms`, `pets-interactions`, `pets-server`,
252
+ `pets-auth-server`, and `pets-publication` entry points add typed, moderated pet
253
+ resources to existing Community Maps collections. Import `pets.css` for the
254
+ scoped presentation. Server adapters require each application's own API key;
255
+ never pass credentials or a `PetScope` to a client component.
256
+
257
+ `PublicationPetsSection` resolves an accepted collection view and its coverage.
258
+ `PublicationPetReferences` resolves `metadata.pet_resources` at render time:
259
+ each reference has `collection_slug`, the API-returned `id`, and `kind`.
260
+ Removed/private resources are omitted; article records need no copied pet data.
261
+ `PublicationPetEvents` extends the existing publication calendar with reviewed
262
+ pet event references and official registration links. Meet's optional `walk`
263
+ field extends its existing dog-walk activity without creating another join system.
264
+
265
+ `createPawAuthHandler` delegates authentication to the platform's supported
266
+ Ledvery OIDC flow. Enable it only after configuring and testing the application
267
+ client and callback. `createPublicationPetsHandler` restricts routes, requires
268
+ same-origin authenticated writes, and proxies visibility-checked photographs.
269
+ Approved sponsorships come from existing Blog campaign delivery and reporting.
270
+ There is no fictional sponsor fallback and no public demonstration inventory.
271
+
272
+ Private recovery contact and broad community alert delivery are not implemented
273
+ by these components. Notification controls default to disabled. The canonical
274
+ application, collections, publication views and legitimate source content must
275
+ be provisioned separately before launch.
276
+
277
+ ### Native pet plugin and shared sign-in
278
+
279
+ `pets-plugin` exports `PetAccountPage`, `PetSubmitPage`, `PetStoryPage` and
280
+ `PetBrowsePage`, with `publicationPetPaths` or `standalonePetPaths`. These are
281
+ server components: give them the publication’s `PetScope` resolved on the server
282
+ and its current reader token. Mount publication pages at `/pets/account`,
283
+ `/pets/submit`, `/pets/:id` and `/pets/browse/:kind`. `native` on the publication
284
+ section, references and calendar keeps resource links inside the publication.
285
+
286
+ Configure `createPawAuthHandler` with the host’s existing `sessionCookie`,
287
+ `callbackPath` and `accountPath`. An existing callback should dispatch to this
288
+ handler only when `openPawFlow(pawCookie(request, PAW_FLOW_COOKIE), state, key)`
289
+ validates. Otherwise preserve the publication’s normal authentication flow.
290
+ `GET resume?next=...` makes a top-level `prompt=none` attempt; protected pages
291
+ should call it only when the session is absent/invalid, `signin` is absent and
292
+ `PAW_SIGNED_OUT_COOKIE` is absent. Failed continuity returns `signin=required`
293
+ or `signin=retry`, preventing redirect loops. Explicit `POST start` opens the
294
+ normal sign-in. Honor and clear the sign-out marker in existing host login/logout
295
+ flows. All cookies remain host-only; do not embed the sign-in in an iframe.
296
+
297
+ The collection must explicitly set `shared_identity_provider` to its configured
298
+ OIDC provider. Auth resolves the verified provider subject; Knowledge authorizes
299
+ each accepted view and keeps ownership collection-scoped. Never correlate local
300
+ users by email or forward one application’s reader token/API key to another.
301
+ Provision and verify each RP before enabling sign-in or contribution flags.
@@ -0,0 +1,62 @@
1
+ import { safeSponsorUrl } from './chunk-66GI2IX6.js';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ function SponsorAcknowledgment({
5
+ creative,
6
+ label = "Community Partner",
7
+ preview = false,
8
+ acknowledgment
9
+ }) {
10
+ const logo = safeSponsorUrl(creative.logo_url);
11
+ const destination = safeSponsorUrl(creative.destination_url);
12
+ return /* @__PURE__ */ jsxs(
13
+ "aside",
14
+ {
15
+ className: "sm-sponsorship",
16
+ "aria-label": `${label}: ${creative.name}`,
17
+ "data-sponsorship": preview ? void 0 : acknowledgment?.campaign_id,
18
+ "data-placement-id": preview ? void 0 : acknowledgment?.placement_id,
19
+ "data-publication-id": preview ? void 0 : acknowledgment?.publication_id,
20
+ "data-sponsor-id": preview ? void 0 : acknowledgment?.sponsor_id,
21
+ children: [
22
+ /* @__PURE__ */ jsxs("div", { className: "sm-sponsorship-copy", children: [
23
+ /* @__PURE__ */ jsxs("p", { className: "sm-sponsorship-label", children: [
24
+ label,
25
+ preview ? " \xB7 Illustrative preview" : ""
26
+ ] }),
27
+ /* @__PURE__ */ jsx("p", { className: "sm-sponsorship-name", children: creative.name }),
28
+ creative.description && /* @__PURE__ */ jsx("p", { className: "sm-sponsorship-description", children: creative.description }),
29
+ destination && /* @__PURE__ */ jsxs(
30
+ "a",
31
+ {
32
+ href: destination,
33
+ rel: "sponsored noopener noreferrer",
34
+ referrerPolicy: "no-referrer",
35
+ "data-analytics-skip": "true",
36
+ children: [
37
+ creative.link_label || "Learn more",
38
+ /* @__PURE__ */ jsxs("span", { className: "sm-sponsorship-sr", children: [
39
+ " about ",
40
+ creative.name
41
+ ] })
42
+ ]
43
+ }
44
+ )
45
+ ] }),
46
+ logo && /* @__PURE__ */ jsx(
47
+ "img",
48
+ {
49
+ src: logo,
50
+ alt: `${creative.name} logo`,
51
+ loading: "lazy",
52
+ decoding: "async",
53
+ referrerPolicy: "no-referrer",
54
+ className: "sm-sponsorship-logo"
55
+ }
56
+ )
57
+ ]
58
+ }
59
+ );
60
+ }
61
+
62
+ export { SponsorAcknowledgment };
@@ -0,0 +1,251 @@
1
+ import { PET_UUID, petReferences, PET_SLUG } from './chunk-AFJ6S5WJ.js';
2
+ import { buildClientContextHeaders } from '@scalemule/sdk';
3
+
4
+ var PetRequestError = class extends Error {
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.status = status;
8
+ }
9
+ status;
10
+ };
11
+ var petPrivateHeaders = {
12
+ "cache-control": "private, no-store",
13
+ "x-robots-tag": "noindex, nofollow",
14
+ "referrer-policy": "no-referrer",
15
+ "x-content-type-options": "nosniff"
16
+ };
17
+ async function bounded(request, max) {
18
+ if (Number(request.headers.get("content-length")) > max)
19
+ throw new PetRequestError(413, "The submission is too large.");
20
+ const reader = request.body?.getReader();
21
+ if (!reader) return new Uint8Array();
22
+ const parts = [];
23
+ let size = 0;
24
+ try {
25
+ for (; ; ) {
26
+ const { done, value } = await reader.read();
27
+ if (done) break;
28
+ size += value.length;
29
+ if (size > max) {
30
+ await reader.cancel();
31
+ throw new PetRequestError(413, "The submission is too large.");
32
+ }
33
+ parts.push(value);
34
+ }
35
+ } finally {
36
+ reader.releaseLock();
37
+ }
38
+ return Buffer.concat(parts);
39
+ }
40
+ function config(scope) {
41
+ const url = new URL(scope.gatewayUrl);
42
+ if (!scope.apiKey || !PET_SLUG.test(scope.collectionSlug) || url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname)))
43
+ throw new PetRequestError(503, "The pet community is not available yet.");
44
+ return url.origin;
45
+ }
46
+ async function petRequest(scope, path, init = {}, fetcher = fetch) {
47
+ const gateway = config(scope);
48
+ if (!path.startsWith("/v1/") || path.includes("..") || /[\\\r\n]/.test(path))
49
+ throw new Error("Invalid platform path");
50
+ const response = await fetcher(gateway + path, {
51
+ method: init.method || "GET",
52
+ headers: {
53
+ ...buildClientContextHeaders(scope.clientContext),
54
+ "x-api-key": scope.apiKey,
55
+ "content-type": "application/json",
56
+ ...scope.accessToken ? { authorization: `Bearer ${scope.accessToken}` } : {}
57
+ },
58
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
59
+ cache: "no-store",
60
+ redirect: "error",
61
+ signal: AbortSignal.timeout(15e3)
62
+ });
63
+ if (!response.ok)
64
+ throw new PetRequestError(
65
+ response.status,
66
+ response.status === 401 ? "Please sign in to continue." : response.status === 404 ? "This pet resource is not available." : response.status === 409 ? "This contribution changed. Reload before saving." : response.status === 429 ? "Please wait before trying again." : "We couldn\u2019t complete that request. Please try again."
67
+ );
68
+ const json = await response.json();
69
+ return json?.data ?? json;
70
+ }
71
+ function createPublicationPetsHandler(options) {
72
+ const fetcher = options.fetch || fetch;
73
+ return async function handler(request, path) {
74
+ const json = (data, status = 200) => Response.json(data, { status, headers: petPrivateHeaders });
75
+ try {
76
+ const scope = await options.resolve(request), gateway = config(scope), method = request.method;
77
+ const root = `/v1/knowledge/community-maps/collections/${scope.collectionSlug}`;
78
+ const id = path[0] && PET_UUID.test(path[0]);
79
+ const isMedia = id && path.length === 3 && path[1] === "media" && PET_UUID.test(path[2]);
80
+ const upload = path.length === 3 && path[0] === "submissions" && PET_UUID.test(path[1]) && path[2] === "media";
81
+ const ownMedia = path.length === 4 && path[0] === "submissions" && PET_UUID.test(path[1]) && path[2] === "media" && PET_UUID.test(path[3]);
82
+ const own = path[0] === "mine" && (path.length === 1 || path.length === 2 && PET_UUID.test(path[1]));
83
+ const preferences = path.length === 1 && path[0] === "preferences";
84
+ const reaction = id && path.length === 2 && path[1] === "reaction";
85
+ const report = id && path.length === 2 && path[1] === "reports";
86
+ const publicRead = method === "GET" && (!path.length || id && path.length === 1 || isMedia || path.length === 1 && path[0] === "community");
87
+ const read = publicRead || method === "GET" && (own || ownMedia || preferences || reaction);
88
+ const write = method === "POST" && (!path.length || upload || report || id && path.length === 2 && path[1] === "reunited") || method === "PUT" && (preferences || reaction) || ["PUT", "DELETE"].includes(method) && id && path.length === 1;
89
+ if (!read && !write) return json({ error: "Route not found" }, 404);
90
+ if (write && (!scope.writesEnabled || request.headers.get("origin") !== new URL(scope.origin).origin))
91
+ return json(
92
+ { error: "Continue from the pet community\u2019s official website." },
93
+ 403
94
+ );
95
+ if ((write || own || ownMedia || preferences || reaction) && !scope.accessToken)
96
+ return json({ error: "Sign in to continue." }, 401);
97
+ let endpoint = root + (path[0] === "community" ? "" : upload || ownMedia ? "/" + path.join("/") : "/pets" + (path.length ? "/" + path.join("/") : ""));
98
+ const query = new URLSearchParams();
99
+ if (method === "GET" && !path.length)
100
+ for (const key of [
101
+ "area_id",
102
+ "kind",
103
+ "after",
104
+ "week",
105
+ "limit",
106
+ "category",
107
+ "species",
108
+ "event_type",
109
+ "cost",
110
+ "starts_after",
111
+ "starts_before"
112
+ ]) {
113
+ const value = new URL(request.url).searchParams.get(key);
114
+ if (value) query.set(key, value);
115
+ }
116
+ if (own) {
117
+ const after = new URL(request.url).searchParams.get("after");
118
+ if (after && PET_UUID.test(after)) query.set("after", after);
119
+ }
120
+ if (query.size) endpoint += "?" + query;
121
+ if (isMedia) {
122
+ const imageQuery = new URLSearchParams();
123
+ const sourceQuery = new URL(request.url).searchParams;
124
+ if (["640", "1280"].includes(sourceQuery.get("width") || ""))
125
+ imageQuery.set("width", sourceQuery.get("width"));
126
+ if (["jpeg", "webp"].includes(sourceQuery.get("format") || ""))
127
+ imageQuery.set("format", sourceQuery.get("format"));
128
+ if (imageQuery.size) endpoint += "?" + imageQuery;
129
+ }
130
+ if (isMedia || upload || ownMedia) {
131
+ let body2;
132
+ const type = request.headers.get("content-type") || "";
133
+ if (upload) {
134
+ if (!type.startsWith("multipart/form-data;"))
135
+ throw new PetRequestError(415, "Choose a photograph.");
136
+ body2 = await bounded(request, 9 * 1024 * 1024);
137
+ }
138
+ const response = await fetcher(gateway + endpoint, {
139
+ method,
140
+ headers: {
141
+ ...buildClientContextHeaders(scope.clientContext),
142
+ "x-api-key": scope.apiKey,
143
+ ...scope.accessToken ? { authorization: `Bearer ${scope.accessToken}` } : {},
144
+ ...upload ? { "content-type": type } : {}
145
+ },
146
+ body: body2,
147
+ cache: "no-store",
148
+ redirect: "error",
149
+ signal: AbortSignal.timeout(45e3)
150
+ });
151
+ if (!response.ok) {
152
+ await response.body?.cancel();
153
+ return json(
154
+ {
155
+ error: "The photograph is unavailable. Check its size and try again."
156
+ },
157
+ response.status >= 500 ? 503 : response.status
158
+ );
159
+ }
160
+ if (upload) return json(await response.json());
161
+ const imageType = response.headers.get("content-type")?.split(";")[0] || "";
162
+ if (!["image/png", "image/jpeg", "image/webp"].includes(imageType)) {
163
+ await response.body?.cancel();
164
+ return json({ error: "Photograph unavailable" }, 404);
165
+ }
166
+ return new Response(response.body, {
167
+ headers: { ...petPrivateHeaders, "content-type": imageType }
168
+ });
169
+ }
170
+ let body;
171
+ if (write) {
172
+ if (!request.headers.get("content-type")?.startsWith("application/json"))
173
+ throw new PetRequestError(415, "Use the pet contribution form.");
174
+ try {
175
+ body = JSON.parse(
176
+ new TextDecoder().decode(await bounded(request, 32768))
177
+ );
178
+ } catch (error) {
179
+ if (error instanceof PetRequestError) throw error;
180
+ throw new PetRequestError(400, "Check the contribution fields.");
181
+ }
182
+ }
183
+ return json({
184
+ data: await petRequest(scope, endpoint, { method, body }, fetcher)
185
+ });
186
+ } catch (error) {
187
+ return json(
188
+ {
189
+ error: error instanceof PetRequestError ? error.message : "The pet community is temporarily unavailable."
190
+ },
191
+ error instanceof PetRequestError ? error.status : 503
192
+ );
193
+ }
194
+ };
195
+ }
196
+ async function resolvePetReferences(scope, metadata, fetcher = fetch) {
197
+ const references = petReferences(metadata).filter(
198
+ (ref) => ref.collection_slug === scope.collectionSlug
199
+ );
200
+ const results = await Promise.all(
201
+ references.map(async (ref) => {
202
+ try {
203
+ const resource = await petRequest(
204
+ { ...scope, accessToken: void 0 },
205
+ `/v1/knowledge/community-maps/collections/${ref.collection_slug}/pets/${ref.id}`,
206
+ {},
207
+ fetcher
208
+ );
209
+ return resource.kind === ref.kind ? resource : null;
210
+ } catch (error) {
211
+ if (error instanceof PetRequestError && [403, 404].includes(error.status))
212
+ return null;
213
+ throw error;
214
+ }
215
+ })
216
+ );
217
+ return results.filter(
218
+ (resource) => resource !== null
219
+ );
220
+ }
221
+ async function petAcknowledgments(scope, community, productPath = "/pets", fetcher = fetch) {
222
+ if (!scope.publicationId || !PET_UUID.test(scope.publicationId)) return [];
223
+ try {
224
+ const groups = await Promise.all(
225
+ ["community_partner", "community_guide"].map(
226
+ async (placement) => {
227
+ const query = new URLSearchParams({
228
+ placement_type: placement,
229
+ product_path: productPath
230
+ });
231
+ if (community) query.set("community", community);
232
+ const response = await petRequest(
233
+ { ...scope, accessToken: void 0 },
234
+ `/v1/blog/publications/${scope.publicationId}/sponsorships/delivery?${query}`,
235
+ {},
236
+ fetcher
237
+ );
238
+ return response.items.filter(
239
+ (item) => item.publication_id === scope.publicationId && item.placement_type === placement
240
+ );
241
+ }
242
+ )
243
+ );
244
+ return groups.flat().slice(0, 2);
245
+ } catch {
246
+ console.warn("paw.sponsorship_delivery_unavailable");
247
+ return [];
248
+ }
249
+ }
250
+
251
+ export { PetRequestError, createPublicationPetsHandler, petAcknowledgments, petPrivateHeaders, petRequest, resolvePetReferences };
@@ -0,0 +1,124 @@
1
+ // src/pets.ts
2
+ var PET_KINDS = [
3
+ "profile",
4
+ "spotlight",
5
+ "photo",
6
+ "celebration",
7
+ "lost",
8
+ "found",
9
+ "adoption",
10
+ "event"
11
+ ];
12
+ var PET_NOTIFICATION_CATEGORIES = [
13
+ ["lost_found", "Lost & found"],
14
+ ["walks", "Paw Walks"],
15
+ ["adoption_events", "Adoption events"],
16
+ ["celebrations", "Pet celebrations"],
17
+ ["spotlight", "Paw of the Week"],
18
+ ["community", "Community information"]
19
+ ];
20
+ var PET_PHOTO_CATEGORIES = [
21
+ ["cute", "Cute pets"],
22
+ ["funny", "Funny moments"],
23
+ ["adventures", "Adventures"],
24
+ ["senior", "Senior pets"],
25
+ ["rescue", "Rescue stories"],
26
+ ["young", "Puppies & kittens"],
27
+ ["unusual", "Unusual pets"],
28
+ ["friends", "Best friends"],
29
+ ["holiday", "Holiday pets"]
30
+ ];
31
+ var PET_CELEBRATIONS = [
32
+ ["birthday", "Pet birthday"],
33
+ ["adoption_anniversary", "Adoption anniversary"],
34
+ ["welcome_home", "Welcome home"],
35
+ ["rescue_story", "Rescue story"],
36
+ ["happy_reunion", "Happy reunion"],
37
+ ["memorial", "Memorial tribute"]
38
+ ];
39
+ var PET_SAFETY = {
40
+ recovery: "Bay Area Paw does not independently verify pet ownership. A report is a contributor\u2019s account. Never send money or verification codes to someone claiming to have found a pet. Keep microchip numbers and ownership evidence private.",
41
+ walks: "Walks are independently organized. Bay Area Paw does not supervise meetups or guarantee participants\u2019 safety. Meet in a public place, follow leash rules and respect other people and animals.",
42
+ adoption: "Contact the named shelter or rescue for current availability, eligibility and adoption arrangements. Bay Area Paw is not the adoption agency and does not collect adoption payments.",
43
+ wellness: "Wellness information is educational. Contact a veterinarian for advice about your pet."
44
+ };
45
+ var PET_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;
46
+ var PET_SLUG = /^[a-z0-9][a-z0-9-]{0,190}$/;
47
+ function petReferences(metadata) {
48
+ const entries = metadata && typeof metadata === "object" ? metadata.pet_resources : null;
49
+ if (!Array.isArray(entries)) return [];
50
+ return entries.slice(0, 12).flatMap((v) => {
51
+ if (!v || typeof v !== "object") return [];
52
+ const r = v;
53
+ return typeof r.collection_slug === "string" && PET_SLUG.test(r.collection_slug) && typeof r.id === "string" && PET_UUID.test(r.id) && PET_KINDS.includes(r.kind) ? [
54
+ {
55
+ collection_slug: r.collection_slug,
56
+ id: r.id,
57
+ kind: r.kind
58
+ }
59
+ ] : [];
60
+ });
61
+ }
62
+ function resolvePetCommunity(areas, selected, preference, publicationDefault) {
63
+ if (selected === "" || selected === void 0 && preference === "all") return void 0;
64
+ for (const candidate of [selected, preference, publicationDefault]) {
65
+ const found = areas.find(
66
+ (area) => area.id === candidate || area.slug === candidate
67
+ );
68
+ if (found) return found;
69
+ }
70
+ return void 0;
71
+ }
72
+ function safePetLink(raw) {
73
+ if (raw.startsWith("/") && !raw.startsWith("//") && !/[\\\r\n]/.test(raw))
74
+ return raw;
75
+ try {
76
+ const url = new URL(raw);
77
+ return url.protocol === "https:" && !url.username && !url.password ? url.href : void 0;
78
+ } catch {
79
+ return void 0;
80
+ }
81
+ }
82
+ function adoptionAvailability(source, now = Date.now()) {
83
+ const at = source?.verified_at ? Date.parse(source.verified_at) : NaN;
84
+ return source?.availability_confirmed && Number.isFinite(at) && at <= now && now - at <= 864e5 ? "confirmed" : "check_with_organization";
85
+ }
86
+ function petProfileMetadata(resource, canonicalUrl, photoUrl) {
87
+ const canonical = safePetLink(canonicalUrl);
88
+ if (!resource || !canonical?.startsWith("https://"))
89
+ return {
90
+ title: "Pet profile unavailable",
91
+ robots: { index: false, follow: false }
92
+ };
93
+ return {
94
+ title: `Meet ${resource.content.name}`,
95
+ description: Array.from(resource.content.story).slice(0, 180).join(""),
96
+ alternates: { canonical },
97
+ openGraph: {
98
+ title: `Meet ${resource.content.name}`,
99
+ description: resource.content.story,
100
+ url: canonical,
101
+ ...photoUrl && safePetLink(photoUrl) ? { images: [{ url: photoUrl, alt: resource.content.name }] } : {}
102
+ }
103
+ };
104
+ }
105
+ function preparePawWalk(value) {
106
+ if (!Number.isInteger(value.duration_minutes) || value.duration_minutes < 10 || value.duration_minutes > 240)
107
+ throw new Error("Choose a walk duration from 10 to 240 minutes.");
108
+ if (value.distance_km !== void 0 && (!Number.isFinite(value.distance_km) || value.distance_km < 0 || value.distance_km > 30))
109
+ throw new Error("Choose a walking distance from 0 to 30 km.");
110
+ if (value.leash !== "required" && value.leash !== "venue_rules")
111
+ throw new Error("Choose the leash requirements.");
112
+ for (const field of [value.size_guidance, value.accessibility])
113
+ if (typeof field !== "string" || Array.from(field).length > 500)
114
+ throw new Error("Keep walk guidance within 500 characters.");
115
+ return {
116
+ duration_minutes: value.duration_minutes,
117
+ distance_km: value.distance_km,
118
+ leash: value.leash,
119
+ size_guidance: value.size_guidance,
120
+ accessibility: value.accessibility
121
+ };
122
+ }
123
+
124
+ export { PET_CELEBRATIONS, PET_KINDS, PET_NOTIFICATION_CATEGORIES, PET_PHOTO_CATEGORIES, PET_SAFETY, PET_SLUG, PET_UUID, adoptionAvailability, petProfileMetadata, petReferences, preparePawWalk, resolvePetCommunity, safePetLink };
@@ -1,3 +1,5 @@
1
+ import { preparePawWalk } from './chunk-AFJ6S5WJ.js';
2
+
1
3
  // src/meet.ts
2
4
  var MEET_ACTIVITIES = [
3
5
  { value: "coffee", label: "Coffee" },
@@ -57,6 +59,13 @@ function prepareMeetup(input, places, now = /* @__PURE__ */ new Date()) {
57
59
  if (!notes.ok) throw new Error(notes.reason);
58
60
  const capacity = Number(input.capacity ?? 6);
59
61
  if (!Number.isInteger(capacity) || capacity < 2 || capacity > 12) throw new Error("Choose a group size from 2 to 12.");
62
+ const walk = activity === "dogs" && input.walk ? preparePawWalk(input.walk) : void 0;
63
+ if (walk) for (const guidance of [walk.size_guidance, walk.accessibility]) {
64
+ if (guidance.trim()) {
65
+ const screened = screenMessage(guidance);
66
+ if (!screened.ok) throw new Error(screened.reason);
67
+ }
68
+ }
60
69
  return {
61
70
  activity,
62
71
  activityLabel: label,
@@ -65,7 +74,8 @@ function prepareMeetup(input, places, now = /* @__PURE__ */ new Date()) {
65
74
  placeDetail: place.detail,
66
75
  startsAt: new Date(starts).toISOString(),
67
76
  notes: notes.text,
68
- capacity
77
+ capacity,
78
+ ...walk ? { walk } : {}
69
79
  };
70
80
  }
71
81