@scalemule/news-publication 0.0.7

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/cookie-consent.css +419 -0
  4. package/dist/chunk-C7JTEHTH.js +158 -0
  5. package/dist/chunk-GB36YM56.js +72 -0
  6. package/dist/chunk-PYCU2T5O.js +93 -0
  7. package/dist/chunk-R33MJWND.js +261 -0
  8. package/dist/chunk-TJ7RL3PV.js +252 -0
  9. package/dist/chunk-YQXS2KQI.js +112 -0
  10. package/dist/cookie-consent-banner.cjs +210 -0
  11. package/dist/cookie-consent-banner.d.cts +19 -0
  12. package/dist/cookie-consent-banner.d.ts +19 -0
  13. package/dist/cookie-consent-banner.js +3 -0
  14. package/dist/cookie-settings-view.cjs +470 -0
  15. package/dist/cookie-settings-view.d.cts +15 -0
  16. package/dist/cookie-settings-view.d.ts +15 -0
  17. package/dist/cookie-settings-view.js +3 -0
  18. package/dist/index.cjs +1460 -0
  19. package/dist/index.d.cts +317 -0
  20. package/dist/index.d.ts +317 -0
  21. package/dist/index.js +441 -0
  22. package/dist/marketplace-Ch7mdJZv.d.cts +123 -0
  23. package/dist/marketplace-Ch7mdJZv.d.ts +123 -0
  24. package/dist/marketplace-forms.cjs +134 -0
  25. package/dist/marketplace-forms.d.cts +10 -0
  26. package/dist/marketplace-forms.d.ts +10 -0
  27. package/dist/marketplace-forms.js +115 -0
  28. package/dist/marketplace-view.cjs +110 -0
  29. package/dist/marketplace-view.d.cts +21 -0
  30. package/dist/marketplace-view.d.ts +21 -0
  31. package/dist/marketplace-view.js +85 -0
  32. package/dist/meet-5Ym5bxfh.d.cts +79 -0
  33. package/dist/meet-5Ym5bxfh.d.ts +79 -0
  34. package/dist/meet-forms.cjs +188 -0
  35. package/dist/meet-forms.d.cts +13 -0
  36. package/dist/meet-forms.d.ts +13 -0
  37. package/dist/meet-forms.js +166 -0
  38. package/dist/meet-view.cjs +82 -0
  39. package/dist/meet-view.d.cts +23 -0
  40. package/dist/meet-view.d.ts +23 -0
  41. package/dist/meet-view.js +59 -0
  42. package/dist/snapshot-BS4hn1dk.d.cts +234 -0
  43. package/dist/snapshot-BS4hn1dk.d.ts +234 -0
  44. package/dist/snapshot-file.cjs +192 -0
  45. package/dist/snapshot-file.d.cts +21 -0
  46. package/dist/snapshot-file.d.ts +21 -0
  47. package/dist/snapshot-file.js +38 -0
  48. package/marketplace.css +61 -0
  49. package/package.json +103 -0
  50. package/tokens/broadsheet.css +61 -0
@@ -0,0 +1,234 @@
1
+ /** Public identity and behavior for one local news site. */
2
+ type NewsLayoutId = "broadsheet" | (string & {});
3
+ declare const FEATURE_NAMES: readonly ["smile", "comments", "chat", "jobs", "classifieds", "referrals", "coupons"];
4
+ type FeatureName = (typeof FEATURE_NAMES)[number];
5
+ type FeatureFlags = Partial<Record<FeatureName, boolean>>;
6
+ type PublicationTheme = {
7
+ accent: string;
8
+ ink: string;
9
+ paper: string;
10
+ sponsor: string;
11
+ };
12
+ type CouponOffer = {
13
+ code: string;
14
+ label: string;
15
+ /** Percent off the plan price, 0–100. */
16
+ percentOff?: number;
17
+ amountOffCents?: number;
18
+ months?: number;
19
+ planSlug?: string;
20
+ };
21
+ type ReferralSettings = {
22
+ enabled: boolean;
23
+ /** Query parameter read on the landing URL. Default `ref`. */
24
+ queryParam: string;
25
+ };
26
+ type CouponSettings = {
27
+ enabled: boolean;
28
+ offers: CouponOffer[];
29
+ /** Site-owned matcher. Used only when coupons are enabled. */
30
+ resolve?: (code: string, offers: CouponOffer[]) => CouponOffer | null;
31
+ };
32
+ /** Named mounts a paper renders itself. The package records them; it does not render UI. */
33
+ type PublicationModules = {
34
+ home?: string[];
35
+ articleEnd?: string[];
36
+ };
37
+ type PublicationInput = {
38
+ slug: string;
39
+ name: string;
40
+ masthead: string[];
41
+ region: string;
42
+ regionLabel: string;
43
+ towns: string[];
44
+ nearbyTowns?: string[];
45
+ sections?: string[];
46
+ description: string;
47
+ tagline?: string;
48
+ briefTitle?: string;
49
+ parent?: boolean;
50
+ /** Default presentation. Crawlers always receive this layout. */
51
+ layout?: NewsLayoutId;
52
+ /** Layouts a reader may choose. The default is always included. */
53
+ layouts?: NewsLayoutId[];
54
+ features?: FeatureFlags;
55
+ theme?: Partial<PublicationTheme>;
56
+ referrals?: Partial<ReferralSettings>;
57
+ coupons?: Partial<Omit<CouponSettings, "enabled">> & {
58
+ enabled?: boolean;
59
+ };
60
+ modules?: PublicationModules;
61
+ /** Extra tag rule owned by the paper, such as Walnut Creek neighborhood keywords. */
62
+ matchTag?: (article: NewsItem, tag: string) => boolean;
63
+ };
64
+ type NewsPublication = {
65
+ slug: string;
66
+ name: string;
67
+ masthead: string[];
68
+ region: string;
69
+ regionLabel: string;
70
+ towns: string[];
71
+ nearbyTowns: string[];
72
+ sections: string[];
73
+ description: string;
74
+ tagline: string;
75
+ briefTitle: string;
76
+ parent: boolean;
77
+ layout: NewsLayoutId;
78
+ layouts: NewsLayoutId[];
79
+ features: Record<FeatureName, boolean>;
80
+ theme: PublicationTheme;
81
+ referrals: ReferralSettings;
82
+ coupons: CouponSettings;
83
+ modules: PublicationModules;
84
+ matchTag?: (article: NewsItem, tag: string) => boolean;
85
+ };
86
+ type TagRef = {
87
+ id?: string;
88
+ name: string;
89
+ slug: string;
90
+ };
91
+ type CategoryRef = {
92
+ id?: string;
93
+ name: string;
94
+ slug: string;
95
+ };
96
+ /** Feed row shape used by Walnut Creek Times. */
97
+ type NewsItem = {
98
+ id: string;
99
+ slug: string;
100
+ title: string;
101
+ excerpt: string | null;
102
+ cover_image_url: string | null;
103
+ published_at: string;
104
+ updated_at: string;
105
+ reading_time_minutes: number;
106
+ no_index: boolean;
107
+ section: string | null;
108
+ author_name: string | null;
109
+ canonical_url?: string | null;
110
+ tags?: TagRef[];
111
+ categories?: CategoryRef[];
112
+ };
113
+ type NewsFeed = {
114
+ items: NewsItem[];
115
+ next_cursor: string | null;
116
+ publication: {
117
+ name: string;
118
+ };
119
+ };
120
+ type CoverImageMetadata = {
121
+ alt?: string;
122
+ focal_point?: {
123
+ x: number;
124
+ y: number;
125
+ };
126
+ ai_generated?: boolean;
127
+ label?: string;
128
+ caption?: string;
129
+ attribution?: string;
130
+ credit?: string;
131
+ };
132
+ type ArticleCitation = {
133
+ url: string;
134
+ title?: string;
135
+ publisher?: string;
136
+ };
137
+ type NewsArticle = NewsItem & {
138
+ content_html: string;
139
+ subtitle?: string;
140
+ seo_title?: string;
141
+ seo_description?: string;
142
+ og_image_url?: string;
143
+ audio?: {
144
+ url?: string;
145
+ duration_ms?: number | null;
146
+ };
147
+ metadata?: {
148
+ cover_image?: CoverImageMetadata;
149
+ citations?: ArticleCitation[];
150
+ resources?: {
151
+ kind: string;
152
+ url: string;
153
+ caption: string;
154
+ attribution: string;
155
+ ai_generated: boolean;
156
+ }[];
157
+ };
158
+ };
159
+ type StoryDisplay = {
160
+ display_title?: string;
161
+ deck?: string;
162
+ brief?: string;
163
+ towns?: string[];
164
+ };
165
+ type NearbyUpdate = {
166
+ slug: string;
167
+ title: string;
168
+ summary: string;
169
+ town: string;
170
+ starts: string;
171
+ ends?: string;
172
+ };
173
+ type HomepageSelection = {
174
+ lead_slug?: string;
175
+ secondary_slugs?: string[];
176
+ brief_slugs?: string[];
177
+ /** ISO timestamp: pinned ordering expires, never silently becomes permanent. */
178
+ pinned_until?: string;
179
+ stories?: Record<string, StoryDisplay>;
180
+ nearby?: NearbyUpdate[];
181
+ };
182
+ type EditorialSection = {
183
+ name: string;
184
+ label: string;
185
+ id: string;
186
+ items: NewsItem[];
187
+ };
188
+
189
+ /**
190
+ * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
191
+ * stay in the site and are attached after the snapshot loads.
192
+ */
193
+ type PublicationRecord = Omit<PublicationInput, "matchTag" | "coupons"> & {
194
+ coupons?: Omit<NonNullable<PublicationInput["coupons"]>, "resolve">;
195
+ };
196
+ /** Last config the application accepted. Age never invalidates it. */
197
+ type PublicationSnapshot = {
198
+ version: 1;
199
+ syncedAt: string;
200
+ record: PublicationRecord;
201
+ };
202
+ type SnapshotHooks = {
203
+ matchTag?: PublicationInput["matchTag"];
204
+ resolveCoupon?: NonNullable<PublicationInput["coupons"]>["resolve"];
205
+ };
206
+ type SyncSource = "fresh" | "retained";
207
+ type SyncedPublication = {
208
+ publication: NewsPublication;
209
+ snapshot: PublicationSnapshot;
210
+ source: SyncSource;
211
+ /** How long this snapshot has been the one on disk. Zero for a fresh sync. */
212
+ ageMs: number;
213
+ };
214
+ declare function publicationRecord(publication: NewsPublication): PublicationRecord;
215
+ declare function snapshotFromRecord(record: PublicationRecord, syncedAt?: string): PublicationSnapshot;
216
+ declare function parseSnapshot(raw: string): PublicationSnapshot | null;
217
+ declare function acceptSnapshot(value: unknown): PublicationSnapshot | null;
218
+ /** A remote payload replaces the snapshot only when it is a complete record for this paper. */
219
+ declare function acceptRemoteRecord(value: unknown, expectedSlug?: string): PublicationRecord | null;
220
+ declare function publicationFromSnapshot(snapshot: PublicationSnapshot, hooks?: SnapshotHooks): NewsPublication;
221
+ /**
222
+ * Fetch the application config. Any failure, timeout, or malformed body keeps
223
+ * the previous snapshot. Nothing here expires a snapshot because the API was
224
+ * unreachable for hours or days.
225
+ */
226
+ declare function syncPublication(input: {
227
+ previous: PublicationSnapshot | null;
228
+ fetchRecord: () => Promise<unknown>;
229
+ expectedSlug?: string;
230
+ now?: number;
231
+ hooks?: SnapshotHooks;
232
+ }): Promise<SyncedPublication>;
233
+
234
+ export { type ArticleCitation as A, type CouponOffer as C, type EditorialSection as E, type FeatureName as F, type HomepageSelection as H, type NewsPublication as N, type PublicationTheme as P, type ReferralSettings as R, type SnapshotHooks as S, type TagRef as T, type PublicationInput as a, type NearbyUpdate as b, type NewsItem as c, type NewsArticle as d, type CategoryRef as e, type CouponSettings as f, type CoverImageMetadata as g, FEATURE_NAMES as h, type FeatureFlags as i, type NewsFeed as j, type NewsLayoutId as k, type PublicationModules as l, type PublicationRecord as m, type PublicationSnapshot as n, type StoryDisplay as o, type SyncSource as p, type SyncedPublication as q, acceptRemoteRecord as r, acceptSnapshot as s, parseSnapshot as t, publicationFromSnapshot as u, publicationRecord as v, snapshotFromRecord as w, syncPublication as x };
@@ -0,0 +1,234 @@
1
+ /** Public identity and behavior for one local news site. */
2
+ type NewsLayoutId = "broadsheet" | (string & {});
3
+ declare const FEATURE_NAMES: readonly ["smile", "comments", "chat", "jobs", "classifieds", "referrals", "coupons"];
4
+ type FeatureName = (typeof FEATURE_NAMES)[number];
5
+ type FeatureFlags = Partial<Record<FeatureName, boolean>>;
6
+ type PublicationTheme = {
7
+ accent: string;
8
+ ink: string;
9
+ paper: string;
10
+ sponsor: string;
11
+ };
12
+ type CouponOffer = {
13
+ code: string;
14
+ label: string;
15
+ /** Percent off the plan price, 0–100. */
16
+ percentOff?: number;
17
+ amountOffCents?: number;
18
+ months?: number;
19
+ planSlug?: string;
20
+ };
21
+ type ReferralSettings = {
22
+ enabled: boolean;
23
+ /** Query parameter read on the landing URL. Default `ref`. */
24
+ queryParam: string;
25
+ };
26
+ type CouponSettings = {
27
+ enabled: boolean;
28
+ offers: CouponOffer[];
29
+ /** Site-owned matcher. Used only when coupons are enabled. */
30
+ resolve?: (code: string, offers: CouponOffer[]) => CouponOffer | null;
31
+ };
32
+ /** Named mounts a paper renders itself. The package records them; it does not render UI. */
33
+ type PublicationModules = {
34
+ home?: string[];
35
+ articleEnd?: string[];
36
+ };
37
+ type PublicationInput = {
38
+ slug: string;
39
+ name: string;
40
+ masthead: string[];
41
+ region: string;
42
+ regionLabel: string;
43
+ towns: string[];
44
+ nearbyTowns?: string[];
45
+ sections?: string[];
46
+ description: string;
47
+ tagline?: string;
48
+ briefTitle?: string;
49
+ parent?: boolean;
50
+ /** Default presentation. Crawlers always receive this layout. */
51
+ layout?: NewsLayoutId;
52
+ /** Layouts a reader may choose. The default is always included. */
53
+ layouts?: NewsLayoutId[];
54
+ features?: FeatureFlags;
55
+ theme?: Partial<PublicationTheme>;
56
+ referrals?: Partial<ReferralSettings>;
57
+ coupons?: Partial<Omit<CouponSettings, "enabled">> & {
58
+ enabled?: boolean;
59
+ };
60
+ modules?: PublicationModules;
61
+ /** Extra tag rule owned by the paper, such as Walnut Creek neighborhood keywords. */
62
+ matchTag?: (article: NewsItem, tag: string) => boolean;
63
+ };
64
+ type NewsPublication = {
65
+ slug: string;
66
+ name: string;
67
+ masthead: string[];
68
+ region: string;
69
+ regionLabel: string;
70
+ towns: string[];
71
+ nearbyTowns: string[];
72
+ sections: string[];
73
+ description: string;
74
+ tagline: string;
75
+ briefTitle: string;
76
+ parent: boolean;
77
+ layout: NewsLayoutId;
78
+ layouts: NewsLayoutId[];
79
+ features: Record<FeatureName, boolean>;
80
+ theme: PublicationTheme;
81
+ referrals: ReferralSettings;
82
+ coupons: CouponSettings;
83
+ modules: PublicationModules;
84
+ matchTag?: (article: NewsItem, tag: string) => boolean;
85
+ };
86
+ type TagRef = {
87
+ id?: string;
88
+ name: string;
89
+ slug: string;
90
+ };
91
+ type CategoryRef = {
92
+ id?: string;
93
+ name: string;
94
+ slug: string;
95
+ };
96
+ /** Feed row shape used by Walnut Creek Times. */
97
+ type NewsItem = {
98
+ id: string;
99
+ slug: string;
100
+ title: string;
101
+ excerpt: string | null;
102
+ cover_image_url: string | null;
103
+ published_at: string;
104
+ updated_at: string;
105
+ reading_time_minutes: number;
106
+ no_index: boolean;
107
+ section: string | null;
108
+ author_name: string | null;
109
+ canonical_url?: string | null;
110
+ tags?: TagRef[];
111
+ categories?: CategoryRef[];
112
+ };
113
+ type NewsFeed = {
114
+ items: NewsItem[];
115
+ next_cursor: string | null;
116
+ publication: {
117
+ name: string;
118
+ };
119
+ };
120
+ type CoverImageMetadata = {
121
+ alt?: string;
122
+ focal_point?: {
123
+ x: number;
124
+ y: number;
125
+ };
126
+ ai_generated?: boolean;
127
+ label?: string;
128
+ caption?: string;
129
+ attribution?: string;
130
+ credit?: string;
131
+ };
132
+ type ArticleCitation = {
133
+ url: string;
134
+ title?: string;
135
+ publisher?: string;
136
+ };
137
+ type NewsArticle = NewsItem & {
138
+ content_html: string;
139
+ subtitle?: string;
140
+ seo_title?: string;
141
+ seo_description?: string;
142
+ og_image_url?: string;
143
+ audio?: {
144
+ url?: string;
145
+ duration_ms?: number | null;
146
+ };
147
+ metadata?: {
148
+ cover_image?: CoverImageMetadata;
149
+ citations?: ArticleCitation[];
150
+ resources?: {
151
+ kind: string;
152
+ url: string;
153
+ caption: string;
154
+ attribution: string;
155
+ ai_generated: boolean;
156
+ }[];
157
+ };
158
+ };
159
+ type StoryDisplay = {
160
+ display_title?: string;
161
+ deck?: string;
162
+ brief?: string;
163
+ towns?: string[];
164
+ };
165
+ type NearbyUpdate = {
166
+ slug: string;
167
+ title: string;
168
+ summary: string;
169
+ town: string;
170
+ starts: string;
171
+ ends?: string;
172
+ };
173
+ type HomepageSelection = {
174
+ lead_slug?: string;
175
+ secondary_slugs?: string[];
176
+ brief_slugs?: string[];
177
+ /** ISO timestamp: pinned ordering expires, never silently becomes permanent. */
178
+ pinned_until?: string;
179
+ stories?: Record<string, StoryDisplay>;
180
+ nearby?: NearbyUpdate[];
181
+ };
182
+ type EditorialSection = {
183
+ name: string;
184
+ label: string;
185
+ id: string;
186
+ items: NewsItem[];
187
+ };
188
+
189
+ /**
190
+ * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
191
+ * stay in the site and are attached after the snapshot loads.
192
+ */
193
+ type PublicationRecord = Omit<PublicationInput, "matchTag" | "coupons"> & {
194
+ coupons?: Omit<NonNullable<PublicationInput["coupons"]>, "resolve">;
195
+ };
196
+ /** Last config the application accepted. Age never invalidates it. */
197
+ type PublicationSnapshot = {
198
+ version: 1;
199
+ syncedAt: string;
200
+ record: PublicationRecord;
201
+ };
202
+ type SnapshotHooks = {
203
+ matchTag?: PublicationInput["matchTag"];
204
+ resolveCoupon?: NonNullable<PublicationInput["coupons"]>["resolve"];
205
+ };
206
+ type SyncSource = "fresh" | "retained";
207
+ type SyncedPublication = {
208
+ publication: NewsPublication;
209
+ snapshot: PublicationSnapshot;
210
+ source: SyncSource;
211
+ /** How long this snapshot has been the one on disk. Zero for a fresh sync. */
212
+ ageMs: number;
213
+ };
214
+ declare function publicationRecord(publication: NewsPublication): PublicationRecord;
215
+ declare function snapshotFromRecord(record: PublicationRecord, syncedAt?: string): PublicationSnapshot;
216
+ declare function parseSnapshot(raw: string): PublicationSnapshot | null;
217
+ declare function acceptSnapshot(value: unknown): PublicationSnapshot | null;
218
+ /** A remote payload replaces the snapshot only when it is a complete record for this paper. */
219
+ declare function acceptRemoteRecord(value: unknown, expectedSlug?: string): PublicationRecord | null;
220
+ declare function publicationFromSnapshot(snapshot: PublicationSnapshot, hooks?: SnapshotHooks): NewsPublication;
221
+ /**
222
+ * Fetch the application config. Any failure, timeout, or malformed body keeps
223
+ * the previous snapshot. Nothing here expires a snapshot because the API was
224
+ * unreachable for hours or days.
225
+ */
226
+ declare function syncPublication(input: {
227
+ previous: PublicationSnapshot | null;
228
+ fetchRecord: () => Promise<unknown>;
229
+ expectedSlug?: string;
230
+ now?: number;
231
+ hooks?: SnapshotHooks;
232
+ }): Promise<SyncedPublication>;
233
+
234
+ export { type ArticleCitation as A, type CouponOffer as C, type EditorialSection as E, type FeatureName as F, type HomepageSelection as H, type NewsPublication as N, type PublicationTheme as P, type ReferralSettings as R, type SnapshotHooks as S, type TagRef as T, type PublicationInput as a, type NearbyUpdate as b, type NewsItem as c, type NewsArticle as d, type CategoryRef as e, type CouponSettings as f, type CoverImageMetadata as g, FEATURE_NAMES as h, type FeatureFlags as i, type NewsFeed as j, type NewsLayoutId as k, type PublicationModules as l, type PublicationRecord as m, type PublicationSnapshot as n, type StoryDisplay as o, type SyncSource as p, type SyncedPublication as q, acceptRemoteRecord as r, acceptSnapshot as s, parseSnapshot as t, publicationFromSnapshot as u, publicationRecord as v, snapshotFromRecord as w, syncPublication as x };
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var promises = require('fs/promises');
5
+ var path = require('path');
6
+
7
+ // src/snapshot-file.ts
8
+
9
+ // src/types.ts
10
+ var FEATURE_NAMES = [
11
+ "smile",
12
+ "comments",
13
+ "chat",
14
+ "jobs",
15
+ "classifieds",
16
+ "referrals",
17
+ "coupons"
18
+ ];
19
+
20
+ // src/config.ts
21
+ var BROADSHEET_THEME = {
22
+ accent: "#bb171d",
23
+ ink: "#171917",
24
+ paper: "#ffffff",
25
+ sponsor: "#173d32"
26
+ };
27
+ var DISABLED_FEATURES = {
28
+ smile: false,
29
+ comments: false,
30
+ chat: false,
31
+ jobs: false,
32
+ classifieds: false,
33
+ referrals: false,
34
+ coupons: false
35
+ };
36
+ function definePublication(input) {
37
+ if (!input.slug.trim() || !input.name.trim()) throw new Error("A publication needs a slug and a name");
38
+ if (!input.towns.length) throw new Error("A publication needs at least one covered town");
39
+ const layout = input.layout ?? "broadsheet";
40
+ const layouts = [.../* @__PURE__ */ new Set([layout, ...input.layouts ?? ["broadsheet"]])];
41
+ const referralsEnabled = input.features?.referrals === true || input.referrals?.enabled === true;
42
+ const couponsEnabled = input.features?.coupons === true || input.coupons?.enabled === true;
43
+ const features = { ...DISABLED_FEATURES, ...input.features, referrals: referralsEnabled, coupons: couponsEnabled };
44
+ for (const name of FEATURE_NAMES) features[name] = features[name] === true;
45
+ return {
46
+ slug: input.slug,
47
+ name: input.name,
48
+ masthead: input.masthead.length ? input.masthead : [input.name],
49
+ region: input.region,
50
+ regionLabel: input.regionLabel,
51
+ towns: input.towns,
52
+ nearbyTowns: input.nearbyTowns ?? [],
53
+ sections: input.sections ?? [],
54
+ description: input.description,
55
+ tagline: input.tagline ?? "Local stories. Close to home.",
56
+ briefTitle: input.briefTitle ?? `${input.regionLabel} in Brief`,
57
+ parent: input.parent === true,
58
+ layout,
59
+ layouts,
60
+ features,
61
+ theme: { ...BROADSHEET_THEME, ...input.theme },
62
+ referrals: { queryParam: input.referrals?.queryParam?.trim() || "ref", enabled: referralsEnabled },
63
+ coupons: { enabled: couponsEnabled, offers: input.coupons?.offers ?? [], resolve: input.coupons?.resolve },
64
+ modules: input.modules ?? {},
65
+ matchTag: input.matchTag
66
+ };
67
+ }
68
+
69
+ // src/snapshot.ts
70
+ var SNAPSHOT_VERSION = 1;
71
+ function publicationRecord(publication) {
72
+ return {
73
+ slug: publication.slug,
74
+ name: publication.name,
75
+ masthead: publication.masthead,
76
+ region: publication.region,
77
+ regionLabel: publication.regionLabel,
78
+ towns: publication.towns,
79
+ nearbyTowns: publication.nearbyTowns,
80
+ sections: publication.sections,
81
+ description: publication.description,
82
+ tagline: publication.tagline,
83
+ briefTitle: publication.briefTitle,
84
+ parent: publication.parent,
85
+ layout: publication.layout,
86
+ layouts: publication.layouts,
87
+ features: publication.features,
88
+ theme: publication.theme,
89
+ referrals: publication.referrals,
90
+ coupons: { enabled: publication.coupons.enabled, offers: publication.coupons.offers },
91
+ modules: publication.modules
92
+ };
93
+ }
94
+ function snapshotFromRecord(record, syncedAt = (/* @__PURE__ */ new Date()).toISOString()) {
95
+ const publication = definePublication(record);
96
+ return { version: SNAPSHOT_VERSION, syncedAt, record: publicationRecord(publication) };
97
+ }
98
+ function parseSnapshot(raw) {
99
+ try {
100
+ return acceptSnapshot(JSON.parse(raw));
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ function acceptSnapshot(value) {
106
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
107
+ const snapshot = value;
108
+ if (snapshot.version !== SNAPSHOT_VERSION || typeof snapshot.syncedAt !== "string") return null;
109
+ if (!Number.isFinite(Date.parse(snapshot.syncedAt))) return null;
110
+ try {
111
+ return snapshotFromRecord(snapshot.record, snapshot.syncedAt);
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ function acceptRemoteRecord(value, expectedSlug) {
117
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
118
+ const body = value;
119
+ const candidate = body.record ?? body.publication ?? value;
120
+ try {
121
+ const publication = definePublication(candidate);
122
+ if (expectedSlug && publication.slug !== expectedSlug) return null;
123
+ return publicationRecord(publication);
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+ function publicationFromSnapshot(snapshot, hooks = {}) {
129
+ return definePublication({
130
+ ...snapshot.record,
131
+ matchTag: hooks.matchTag,
132
+ coupons: { ...snapshot.record.coupons, resolve: hooks.resolveCoupon }
133
+ });
134
+ }
135
+ async function syncPublication(input) {
136
+ const now = input.now ?? Date.now();
137
+ try {
138
+ const remote = acceptRemoteRecord(await input.fetchRecord(), input.expectedSlug);
139
+ if (remote) {
140
+ const snapshot = snapshotFromRecord(remote, new Date(now).toISOString());
141
+ return { publication: publicationFromSnapshot(snapshot, input.hooks), snapshot, source: "fresh", ageMs: 0 };
142
+ }
143
+ } catch {
144
+ }
145
+ if (!input.previous) {
146
+ throw new Error("This paper has no synced publication config");
147
+ }
148
+ const ageMs = Math.max(0, now - Date.parse(input.previous.syncedAt));
149
+ return {
150
+ publication: publicationFromSnapshot(input.previous, input.hooks),
151
+ snapshot: input.previous,
152
+ source: "retained",
153
+ ageMs
154
+ };
155
+ }
156
+
157
+ // src/snapshot-file.ts
158
+ async function readSnapshotFile(file) {
159
+ try {
160
+ return parseSnapshot(await promises.readFile(file, "utf8"));
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+ async function writeSnapshotFile(file, snapshot) {
166
+ const accepted = acceptSnapshot(snapshot);
167
+ if (!accepted) throw new Error("Refusing to store an incomplete publication snapshot");
168
+ const temporary = path.join(path.dirname(file), `.publication-snapshot-${crypto.randomBytes(6).toString("hex")}.tmp`);
169
+ await promises.writeFile(temporary, `${JSON.stringify(accepted)}
170
+ `, "utf8");
171
+ try {
172
+ await promises.rename(temporary, file);
173
+ } catch (error) {
174
+ await promises.unlink(temporary).catch(() => void 0);
175
+ throw error;
176
+ }
177
+ }
178
+ async function syncPublicationFile(input) {
179
+ const previous = await readSnapshotFile(input.file);
180
+ const synced = await syncPublication({ ...input, previous });
181
+ if (synced.source === "fresh") {
182
+ try {
183
+ await writeSnapshotFile(input.file, synced.snapshot);
184
+ } catch {
185
+ }
186
+ }
187
+ return synced;
188
+ }
189
+
190
+ exports.readSnapshotFile = readSnapshotFile;
191
+ exports.syncPublicationFile = syncPublicationFile;
192
+ exports.writeSnapshotFile = writeSnapshotFile;
@@ -0,0 +1,21 @@
1
+ import { n as PublicationSnapshot, S as SnapshotHooks, q as SyncedPublication } from './snapshot-BS4hn1dk.cjs';
2
+
3
+ declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
4
+ /**
5
+ * Replace the snapshot only after the new file is fully written.
6
+ * A crash or a failed write leaves the previous file in place.
7
+ */
8
+ declare function writeSnapshotFile(file: string, snapshot: PublicationSnapshot): Promise<void>;
9
+ /**
10
+ * Read the shipped snapshot, try the application, and write only a good result.
11
+ * A failed sync returns the file already on disk and does not touch it.
12
+ */
13
+ declare function syncPublicationFile(input: {
14
+ file: string;
15
+ fetchRecord: () => Promise<unknown>;
16
+ expectedSlug?: string;
17
+ now?: number;
18
+ hooks?: SnapshotHooks;
19
+ }): Promise<SyncedPublication>;
20
+
21
+ export { readSnapshotFile, syncPublicationFile, writeSnapshotFile };
@@ -0,0 +1,21 @@
1
+ import { n as PublicationSnapshot, S as SnapshotHooks, q as SyncedPublication } from './snapshot-BS4hn1dk.js';
2
+
3
+ declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
4
+ /**
5
+ * Replace the snapshot only after the new file is fully written.
6
+ * A crash or a failed write leaves the previous file in place.
7
+ */
8
+ declare function writeSnapshotFile(file: string, snapshot: PublicationSnapshot): Promise<void>;
9
+ /**
10
+ * Read the shipped snapshot, try the application, and write only a good result.
11
+ * A failed sync returns the file already on disk and does not touch it.
12
+ */
13
+ declare function syncPublicationFile(input: {
14
+ file: string;
15
+ fetchRecord: () => Promise<unknown>;
16
+ expectedSlug?: string;
17
+ now?: number;
18
+ hooks?: SnapshotHooks;
19
+ }): Promise<SyncedPublication>;
20
+
21
+ export { readSnapshotFile, syncPublicationFile, writeSnapshotFile };