@growth-labs/seo 0.1.5 → 0.2.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 (115) hide show
  1. package/README.md +142 -54
  2. package/dist/bindings.d.ts +127 -0
  3. package/dist/bindings.d.ts.map +1 -0
  4. package/dist/bindings.js +11 -0
  5. package/dist/bindings.js.map +1 -0
  6. package/dist/cron/prune-aeo-r2.d.ts +36 -0
  7. package/dist/cron/prune-aeo-r2.d.ts.map +1 -0
  8. package/dist/cron/prune-aeo-r2.js +94 -0
  9. package/dist/cron/prune-aeo-r2.js.map +1 -0
  10. package/dist/durable-objects/aeo-revalidation-coord.d.ts +69 -0
  11. package/dist/durable-objects/aeo-revalidation-coord.d.ts.map +1 -0
  12. package/dist/durable-objects/aeo-revalidation-coord.js +177 -0
  13. package/dist/durable-objects/aeo-revalidation-coord.js.map +1 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +101 -14
  17. package/dist/index.js.map +1 -1
  18. package/dist/middleware/seo.d.ts +44 -4
  19. package/dist/middleware/seo.d.ts.map +1 -1
  20. package/dist/middleware/seo.js +237 -41
  21. package/dist/middleware/seo.js.map +1 -1
  22. package/dist/options.d.ts +1293 -6
  23. package/dist/options.d.ts.map +1 -1
  24. package/dist/options.js +238 -1
  25. package/dist/options.js.map +1 -1
  26. package/dist/routes/apple-news.d.ts +4 -0
  27. package/dist/routes/apple-news.d.ts.map +1 -0
  28. package/dist/routes/apple-news.js +28 -0
  29. package/dist/routes/apple-news.js.map +1 -0
  30. package/dist/routes/llms-full.d.ts +4 -0
  31. package/dist/routes/llms-full.d.ts.map +1 -0
  32. package/dist/routes/llms-full.js +29 -0
  33. package/dist/routes/llms-full.js.map +1 -0
  34. package/dist/routes/revalidate.d.ts +16 -0
  35. package/dist/routes/revalidate.d.ts.map +1 -0
  36. package/dist/routes/revalidate.js +243 -0
  37. package/dist/routes/revalidate.js.map +1 -0
  38. package/dist/routes/rss.d.ts.map +1 -1
  39. package/dist/routes/rss.js +4 -1
  40. package/dist/routes/rss.js.map +1 -1
  41. package/dist/routes/sitemap-markdown.d.ts +4 -0
  42. package/dist/routes/sitemap-markdown.d.ts.map +1 -0
  43. package/dist/routes/sitemap-markdown.js +32 -0
  44. package/dist/routes/sitemap-markdown.js.map +1 -0
  45. package/dist/types.d.ts +16 -2
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/utils/aeo-summary.d.ts +35 -0
  48. package/dist/utils/aeo-summary.d.ts.map +1 -0
  49. package/dist/utils/aeo-summary.js +141 -0
  50. package/dist/utils/aeo-summary.js.map +1 -0
  51. package/dist/utils/aeo-twin-emitter.d.ts +79 -0
  52. package/dist/utils/aeo-twin-emitter.d.ts.map +1 -0
  53. package/dist/utils/aeo-twin-emitter.js +99 -0
  54. package/dist/utils/aeo-twin-emitter.js.map +1 -0
  55. package/dist/utils/aeo.d.ts +62 -12
  56. package/dist/utils/aeo.d.ts.map +1 -1
  57. package/dist/utils/aeo.js +187 -26
  58. package/dist/utils/aeo.js.map +1 -1
  59. package/dist/utils/apple-news-anf.d.ts +38 -0
  60. package/dist/utils/apple-news-anf.d.ts.map +1 -0
  61. package/dist/utils/apple-news-anf.js +120 -0
  62. package/dist/utils/apple-news-anf.js.map +1 -0
  63. package/dist/utils/apple-news-rss.d.ts +31 -0
  64. package/dist/utils/apple-news-rss.d.ts.map +1 -0
  65. package/dist/utils/apple-news-rss.js +103 -0
  66. package/dist/utils/apple-news-rss.js.map +1 -0
  67. package/dist/utils/content-filter.d.ts +52 -0
  68. package/dist/utils/content-filter.d.ts.map +1 -0
  69. package/dist/utils/content-filter.js +75 -0
  70. package/dist/utils/content-filter.js.map +1 -0
  71. package/dist/utils/crawler-class.d.ts +39 -0
  72. package/dist/utils/crawler-class.d.ts.map +1 -0
  73. package/dist/utils/crawler-class.js +127 -0
  74. package/dist/utils/crawler-class.js.map +1 -0
  75. package/dist/utils/effective-auth.d.ts +28 -0
  76. package/dist/utils/effective-auth.d.ts.map +1 -0
  77. package/dist/utils/effective-auth.js +33 -0
  78. package/dist/utils/effective-auth.js.map +1 -0
  79. package/dist/utils/fcrdns.d.ts +73 -0
  80. package/dist/utils/fcrdns.d.ts.map +1 -0
  81. package/dist/utils/fcrdns.js +219 -0
  82. package/dist/utils/fcrdns.js.map +1 -0
  83. package/dist/utils/fresh-layer.d.ts +53 -0
  84. package/dist/utils/fresh-layer.d.ts.map +1 -0
  85. package/dist/utils/fresh-layer.js +147 -0
  86. package/dist/utils/fresh-layer.js.map +1 -0
  87. package/dist/utils/index.d.ts +14 -3
  88. package/dist/utils/index.d.ts.map +1 -1
  89. package/dist/utils/index.js +14 -3
  90. package/dist/utils/index.js.map +1 -1
  91. package/dist/utils/json-ld/article.d.ts +13 -1
  92. package/dist/utils/json-ld/article.d.ts.map +1 -1
  93. package/dist/utils/json-ld/article.js +37 -8
  94. package/dist/utils/json-ld/article.js.map +1 -1
  95. package/dist/utils/llms-full.d.ts +29 -0
  96. package/dist/utils/llms-full.d.ts.map +1 -0
  97. package/dist/utils/llms-full.js +67 -0
  98. package/dist/utils/llms-full.js.map +1 -0
  99. package/dist/utils/meta.d.ts +4 -1
  100. package/dist/utils/meta.d.ts.map +1 -1
  101. package/dist/utils/meta.js +25 -2
  102. package/dist/utils/meta.js.map +1 -1
  103. package/dist/utils/sitemap-markdown.d.ts +24 -0
  104. package/dist/utils/sitemap-markdown.d.ts.map +1 -0
  105. package/dist/utils/sitemap-markdown.js +57 -0
  106. package/dist/utils/sitemap-markdown.js.map +1 -0
  107. package/dist/utils/staleness.d.ts +27 -0
  108. package/dist/utils/staleness.d.ts.map +1 -0
  109. package/dist/utils/staleness.js +46 -0
  110. package/dist/utils/staleness.js.map +1 -0
  111. package/dist/utils/validation.d.ts +41 -0
  112. package/dist/utils/validation.d.ts.map +1 -1
  113. package/dist/utils/validation.js +78 -0
  114. package/dist/utils/validation.js.map +1 -1
  115. package/package.json +13 -1
package/README.md CHANGED
@@ -1,80 +1,168 @@
1
1
  # @growth-labs/seo
2
2
 
3
- Astro integration for complete SEO infrastructure on Cloudflare. Handles JSON-LD structured data, meta tags, sitemaps, RSS/podcast feeds, AEO (Answer Engine Optimization), multilingual support, robots.txt, llms.txt, and build-time validation.
3
+ Astro integration for complete SEO infrastructure on Cloudflare. Handles JSON-LD structured data, meta tags, sitemaps, RSS/podcast/Apple News feeds, AEO (Answer Engine Optimization) with crawler-class dispatch, multilingual support, robots.txt, llms.txt / llms-full.txt, and build-time validation.
4
4
 
5
- ## Config
5
+ ## Quick start — prerendered content site (WarFronts pattern)
6
+
7
+ For immutable-after-publish content (articles, docs, archived posts). Zero bindings required.
6
8
 
7
9
  ```typescript
8
10
  import seo from '@growth-labs/seo'
9
11
 
12
+ export default defineConfig({
13
+ integrations: [
14
+ seo({
15
+ site: 'https://warfronts.channel',
16
+ organization: {
17
+ name: 'WarFronts',
18
+ logo: 'https://media.warfronts.channel/logos/header.png',
19
+ },
20
+ aeoTwins: true, // → { mode: 'static' }
21
+ llmsTxt: true,
22
+ rss: true,
23
+ contentProvider: async ({ type, slugs }) => loadContent(type, slugs),
24
+ }),
25
+ ],
26
+ })
27
+ ```
28
+
29
+ `mode: 'static'` emits `.md` twin files at build time to `dist/client/article/<slug>.md`. Cloudflare Assets (or any static host) serves them directly. No Worker hop.
30
+
31
+ ## Premium publisher (fronts.co pattern) — SSR, gated content, Flexible Sampling
32
+
33
+ For paid publications where members see the full body and verified Googlebot gets the paywall-marked full body under Google's Flexible Sampling policy.
34
+
35
+ ```typescript
10
36
  seo({
11
- site: 'https://warfronts.channel',
12
- schemaType: 'Article', // Default JSON-LD type
13
- organization: {
14
- name: 'WarFronts',
15
- logo: 'https://media.warfronts.channel/logos/header.png',
16
- sameAs: ['https://youtube.com/@warfronts'],
37
+ site: 'https://fronts.co',
38
+ organization: { name: 'Fronts', logo: 'https://fronts.co/logo.png' },
39
+ aeoTwins: {
40
+ mode: 'middleware',
41
+ onDemandRevalidation: true,
42
+ revalidateToken: import.meta.env.SEO_REVALIDATE_TOKEN, // ≥32 random bytes
43
+ freshLayer: { bindingName: 'AEO_TWINS', type: 'r2' },
17
44
  },
18
- googleNews: false, // Include in Google News sitemap
19
- aeoTwins: true, // Accept: text/markdown content negotiation
20
- llmsTxt: true, // Serve /llms.txt
21
- rss: true, // Serve /feed.xml
22
- contentSignal: { // Content-Signal HTTP header
23
- aiTrain: 'no',
24
- search: 'yes',
25
- aiInput: 'yes',
26
- },
27
- defaults: {
28
- titleSuffix: ' | WarFronts',
29
- twitterSite: '@warfronts',
30
- },
31
- // Optional:
32
- commerce: { enabled: true, currency: 'USD', returnPolicy: { ... } },
33
- podcast: { enabled: true, title: '...', author: '...', ... },
34
- audioNarration: { enabled: true, narratorName: '...', asPodcastFeed: true },
35
- locales: [{ lang: 'en', region: 'US', default: true }], // Multilingual
45
+ flexibleSampling: { enabled: true, sampleMode: 'lead-in', leadInParagraphs: 2 },
46
+ llmsTxt: true,
47
+ llmsFullTxt: true,
48
+ contentProvider: async ({ type, slugs }) => loadContent(type, slugs),
36
49
  })
37
50
  ```
38
51
 
39
- ## What It Injects
52
+ Gated articles need `prerender: false` on their route file — enforced at build time via the prerender-gated-content guard.
40
53
 
41
- **Middleware** (order: `post`):
42
- - Adds `Content-Signal` header to responses
43
- - Adds `Link` alternate header for hreflang
44
- - Handles `Accept: text/markdown` → returns markdown twin (AEO)
54
+ ## wrangler.toml (required for middleware/both modes)
45
55
 
46
- **Routes:**
47
- - `/sitemap-index.xml` sitemap index
48
- - `/sitemap-articles.xml`, `/sitemap-pages.xml`, `/sitemap-videos.xml`
49
- - `/sitemap-products.xml` (if commerce enabled)
50
- - `/robots.txt` — with AI crawler blocking directives
51
- - `/llms.txt` (if enabled)
52
- - `/feed.xml` (if RSS enabled)
53
- - `/podcast.xml` (if podcast enabled)
54
- - `/listen.xml` (if audio narration podcast feed enabled)
56
+ ```toml
57
+ # Fresh-twin storage (R2 preferred, KV acceptable)
58
+ [[r2_buckets]]
59
+ binding = "AEO_TWINS"
60
+ bucket_name = "my-site-aeo-twins"
61
+
62
+ # Revalidation Coordinator — rate limit + per-slug lock + idempotency
63
+ [[durable_objects.bindings]]
64
+ name = "AEO_REVALIDATION_COORD"
65
+ class_name = "AeoRevalidationCoordinator"
66
+
67
+ [[migrations]]
68
+ tag = "v1"
69
+ new_sqlite_classes = ["AeoRevalidationCoordinator"]
70
+
71
+ # Version metadata (R2 key prefixing for rollback safety)
72
+ [version_metadata]
73
+ binding = "CF_VERSION_METADATA"
55
74
 
56
- ## Standalone Utilities
75
+ # Daily prune cron — deletes old-version R2 entries
76
+ [triggers]
77
+ crons = ["0 3 * * *"]
57
78
 
58
- Available without the integration:
79
+ # Assets binding — MUST set not_found_handling to "none" or middleware's
80
+ # env.ASSETS.fetch() for missing twins will return the SPA fallback.
81
+ [assets]
82
+ binding = "ASSETS"
83
+ directory = "./dist/client"
84
+ not_found_handling = "none"
85
+ ```
86
+
87
+ Re-export the DO class + scheduled handler from your Worker entrypoint:
59
88
 
60
89
  ```typescript
61
- import { generateJsonLd, generateMeta } from '@growth-labs/seo/utils'
62
- import { articleJsonLd } from '@growth-labs/seo/utils/json-ld/article'
63
- import { productJsonLd } from '@growth-labs/seo/utils/json-ld/product'
90
+ export { AeoRevalidationCoordinator } from '@growth-labs/seo/durable-objects'
91
+ export { pruneAeoR2 } from '@growth-labs/seo/cron'
92
+
93
+ export default {
94
+ async scheduled(event, env, ctx) {
95
+ await pruneAeoR2({ env })
96
+ },
97
+ }
64
98
  ```
65
99
 
66
- **JSON-LD generators:** Article, NewsArticle, BlogPosting, FAQPage, VideoObject, AudioObject, Person, HowTo, Product, ProductGroup, Review, AggregateRating, BreadcrumbList, Organization, WebSite, ItemList, SpeakableSpecification
100
+ ## What it injects
67
101
 
68
- **Other utilities:** `generateMeta()` (OG + Twitter Card), sitemap XML, RSS/Atom, podcast RSS (iTunes namespace), hreflang tags, robots.txt, llms.txt
102
+ **Middleware** (order: `post`):
103
+ - Classifies every request: verified search crawler (BM fast path + FCrDNS fallback), LLM training crawler, user-directed LLM agent, anonymous.
104
+ - Sets `Astro.locals.crawlerClass` + `effectiveAuthSegment` for consumer cache-key builders.
105
+ - 403s LLM training crawlers on `access: 'members'` items.
106
+ - Adds `Content-Signal` header on every response.
107
+ - Adds `Vary: Accept, User-Agent, Cookie, CF-Connecting-IP` on every SSR response.
108
+ - Adds `Link: rel="alternate"; type="text/markdown"` on HTML responses (suppressed for members items).
109
+ - Serves `.md` twins via `Accept: text/markdown` content-negotiation in middleware/both modes (R2 → Assets → 503 stub + background render fallthrough).
69
110
 
70
- ## Wrangler Bindings
111
+ **Routes:**
112
+ - `/sitemap-index.xml` + `sitemap-articles.xml`, `sitemap-pages.xml`, `sitemap-videos.xml`, `sitemap-products.xml`
113
+ - `/sitemap-markdown.xml` — twin URL sitemap (static/both modes only)
114
+ - `/robots.txt`
115
+ - `/llms.txt`, `/llms-full.txt`
116
+ - `/feed.xml` (RSS)
117
+ - `/apple-news.xml` (Apple News Publisher RSS, if enabled)
118
+ - `/podcast.xml`, `/listen.xml`
119
+ - `POST /_seo/revalidate` — CMS webhook target (when `onDemandRevalidation: true`)
120
+
121
+ **Build-time:**
122
+ - Emits `.md` twins + summary twins for public items (static/both modes) under `dist/client/`.
123
+ - Validates hreflang reciprocity.
124
+ - Validates no prerendered route serves a members-gated item (when Flexible Sampling is enabled).
125
+ - Per-page HTML validation (title length, meta description, canonical, H1, JSON-LD presence).
126
+
127
+ ## Crawler classes
128
+
129
+ | Class | What they see | Notes |
130
+ |---|---|---|
131
+ | `verifiedSearchCrawler` | Full body (+ paywall JSON-LD on gated items under Flexible Sampling) | FCrDNS-verified Googlebot/Bingbot/Applebot. Cloudflare BM fast path when available. |
132
+ | `llmTrainingCrawler` | 403 on members items, public body on public items | GPTBot, ClaudeBot, CCBot, PerplexityBot, Applebot-Extended, etc. |
133
+ | `userDirectedLlmAgent` | Anonymous body only, regardless of cookies | ChatGPT-User, Claude-User, PerplexityBot-User, Google-NotebookLM. Load-bearing override prevents cookie-based leakage. |
134
+ | `anonymous` | Public body or gate | Everything else. |
135
+
136
+ ## Standalone utilities
137
+
138
+ All pure-function utilities are available without the integration:
71
139
 
72
- None required.
140
+ ```typescript
141
+ import { generateArticleJsonLd, generateProductJsonLd } from '@growth-labs/seo/utils'
142
+ import { generateMeta } from '@growth-labs/seo/utils/meta'
143
+ import { generateAppleNewsRss, generateAppleNewsAnf } from '@growth-labs/seo/utils'
144
+ import { classifyRequest, createFcrdnsVerifier } from '@growth-labs/seo/utils'
145
+ import { computeEffectiveAuthSegment } from '@growth-labs/seo/utils'
146
+ ```
73
147
 
74
- ## Key Patterns
148
+ **JSON-LD generators:** Article, NewsArticle, BlogPosting, FAQPage, VideoObject, AudioObject, Person, HowTo, Product, BreadcrumbList, Organization, WebSite, ItemList, SpeakableSpecification
149
+
150
+ **Feed generators:** RSS, Apple News Publisher RSS, Apple News Format (ANF) JSON, podcast RSS, listen feed, llms.txt, llms-full.txt
151
+
152
+ **Utilities:** OG + Twitter Card meta, sitemap XML, markdown sitemap, hreflang, robots.txt, AEO markdown generator with RAG chunk markers, summary twin generator, content-hash staleness.
153
+
154
+ ## Non-Cloudflare hosts
155
+
156
+ `aeoTwins: { mode: 'static' }` works on any host that serves static files (Vercel, Netlify, GitHub Pages, S3+CloudFront). Other modes (`'middleware'`, `'both'`) and `onDemandRevalidation` require Cloudflare Workers + R2/KV + Durable Objects. See [packages-seo-SPEC-v2.md](../../packages-seo-SPEC-v2.md) "Deployment Targets" for details.
157
+
158
+ ## Key patterns
75
159
 
76
160
  - Virtual module: `virtual:growth-labs/seo/config`
77
- - AI crawler blocking happens at Cloudflare CDN level, NOT in this package
78
- - AEO twins: same content as markdown via `Accept: text/markdown` content negotiation
79
- - Build-time validation warns on missing titles, descriptions, JSON-LD issues (non-blocking)
80
- - `.astro` component files ship as source from `src/components/`
161
+ - AI crawler blocking: enforced at `robots.txt` AND per-request 403 on members items
162
+ - `.md` twin canonical: emitted with `X-Robots-Tag: noindex` + `Link: <html-url>; rel="canonical"` prevents Google from clustering the `.md` as a duplicate of the HTML
163
+ - Summary twins: `.summary.md` companion emitted when `summaryTwin: true` (default), with a 4-tier fallback (item.summary → bullets → first-sentence-per-section → description-only)
164
+ - Build-time validation: non-blocking for per-page issues, FAILS the build for gated content on prerendered routes
165
+
166
+ ## Full spec
167
+
168
+ See [packages-seo-SPEC-v2.md](../../packages-seo-SPEC-v2.md) for the complete 2,400-line specification including the test matrix, architectural rationale, and worked examples for every code path.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * R2 bucket binding for the fresh-twin layer.
3
+ * Subset of the R2Bucket API we actually use.
4
+ */
5
+ export interface R2BucketLike {
6
+ get(key: string): Promise<R2ObjectLike | null>;
7
+ put(key: string, value: ReadableStream | ArrayBuffer | string, options?: R2PutOptions): Promise<unknown>;
8
+ delete(keys: string | string[]): Promise<void>;
9
+ list(options?: {
10
+ prefix?: string;
11
+ cursor?: string;
12
+ limit?: number;
13
+ }): Promise<R2ListResult>;
14
+ }
15
+ export interface R2ObjectLike {
16
+ readonly key: string;
17
+ readonly size: number;
18
+ readonly uploaded: Date;
19
+ readonly httpMetadata?: R2HTTPMetadata;
20
+ readonly customMetadata?: Record<string, string>;
21
+ text(): Promise<string>;
22
+ arrayBuffer(): Promise<ArrayBuffer>;
23
+ body: ReadableStream | null;
24
+ bodyUsed: boolean;
25
+ }
26
+ export interface R2PutOptions {
27
+ httpMetadata?: R2HTTPMetadata;
28
+ customMetadata?: Record<string, string>;
29
+ }
30
+ export interface R2HTTPMetadata {
31
+ contentType?: string;
32
+ contentLanguage?: string;
33
+ contentDisposition?: string;
34
+ contentEncoding?: string;
35
+ cacheControl?: string;
36
+ cacheExpiry?: Date;
37
+ }
38
+ export interface R2ListResult {
39
+ objects: R2ObjectLike[];
40
+ truncated: boolean;
41
+ cursor?: string;
42
+ }
43
+ /**
44
+ * KV namespace binding — acceptable alternative to R2 for the fresh-twin layer.
45
+ * Subset of the KVNamespace API we actually use.
46
+ */
47
+ export interface KVNamespaceLike {
48
+ get(key: string): Promise<string | null>;
49
+ get(key: string, options: {
50
+ type: 'text';
51
+ }): Promise<string | null>;
52
+ get(key: string, options: {
53
+ type: 'arrayBuffer';
54
+ }): Promise<ArrayBuffer | null>;
55
+ put(key: string, value: string | ArrayBuffer, options?: {
56
+ expiration?: number;
57
+ expirationTtl?: number;
58
+ metadata?: unknown;
59
+ }): Promise<void>;
60
+ delete(key: string): Promise<void>;
61
+ list(options?: {
62
+ prefix?: string;
63
+ cursor?: string;
64
+ limit?: number;
65
+ }): Promise<{
66
+ keys: Array<{
67
+ name: string;
68
+ }>;
69
+ list_complete: boolean;
70
+ cursor?: string;
71
+ }>;
72
+ }
73
+ /**
74
+ * Assets binding — reads static files from the Worker's deployment bundle without
75
+ * re-entering the Worker's request routing. Use:
76
+ * env.ASSETS.fetch(new Request('https://assets.local/path/to/file.md'))
77
+ * The hostname is ignored by the assets runtime; only pathname matters.
78
+ * Set `assets.not_found_handling = "none"` in wrangler.toml to prevent SPA fallback
79
+ * from serving index.html when the requested path is missing.
80
+ */
81
+ export interface AssetsBinding {
82
+ fetch(request: Request): Promise<Response>;
83
+ }
84
+ /**
85
+ * Durable Object namespace binding for the Revalidation Coordinator. The actual DO
86
+ * class is exported from `@growth-labs/seo/durable-objects` and wired by the consumer
87
+ * in their Worker entrypoint.
88
+ */
89
+ export interface DurableObjectNamespaceLike {
90
+ idFromName(name: string): DurableObjectIdLike;
91
+ get(id: DurableObjectIdLike): DurableObjectStubLike;
92
+ }
93
+ export interface DurableObjectIdLike {
94
+ toString(): string;
95
+ equals(other: DurableObjectIdLike): boolean;
96
+ }
97
+ export interface DurableObjectStubLike {
98
+ fetch(request: Request): Promise<Response>;
99
+ }
100
+ /**
101
+ * Version metadata binding. Injected into `env` as `CF_VERSION_METADATA`.
102
+ * Used for rollback-safe R2 key prefixing (`twin/<id>/<slug>.md`).
103
+ * `id` is an opaque version UUID that rotates on every new deployment.
104
+ * `tag` is an optional user-supplied annotation via `workers/tag`.
105
+ * `timestamp` is the version's creation time (ISO 8601 string).
106
+ * Ref: https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/
107
+ */
108
+ export interface VersionMetadata {
109
+ readonly id: string;
110
+ readonly tag?: string;
111
+ readonly timestamp: string;
112
+ }
113
+ /**
114
+ * The minimal Worker env shape the SEO package depends on. Consumers will have many
115
+ * more bindings than this; we only declare what we read.
116
+ *
117
+ * All fields are optional because the package's `mode: 'static'` code path runs without
118
+ * any Worker bindings. Runtime binding resolvers throw structured errors when required
119
+ * bindings are missing.
120
+ */
121
+ export interface SeoEnv {
122
+ AEO_TWINS?: R2BucketLike | KVNamespaceLike;
123
+ AEO_REVALIDATION_COORD?: DurableObjectNamespaceLike;
124
+ ASSETS?: AssetsBinding;
125
+ CF_VERSION_METADATA?: VersionMetadata;
126
+ }
127
+ //# sourceMappingURL=bindings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ // Cloudflare binding declarations used by the SEO integration at request and build time.
2
+ //
3
+ // These types are not Astro-specific. They describe the subset of `env` the package
4
+ // touches when running inside a Cloudflare Worker. Consumers provide the actual bindings
5
+ // in wrangler.toml; this file gives us a narrow structural type so utility and middleware
6
+ // code can be type-checked without a hard dependency on `@cloudflare/workers-types`.
7
+ //
8
+ // If `@cloudflare/workers-types` is in the consumer's project, their stricter types will
9
+ // widen-compatibly overlap these interfaces.
10
+ export {};
11
+ //# sourceMappingURL=bindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindings.js","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,EAAE;AACF,oFAAoF;AACpF,yFAAyF;AACzF,0FAA0F;AAC1F,qFAAqF;AACrF,EAAE;AACF,yFAAyF;AACzF,6CAA6C"}
@@ -0,0 +1,36 @@
1
+ import type { SeoEnv } from '../bindings.js';
2
+ export interface PruneAeoR2Options {
3
+ env: SeoEnv;
4
+ /** Binding name to read from env. Matches aeoTwins.freshLayer.bindingName. */
5
+ bindingName?: string;
6
+ /** 'r2' (default) or 'kv'. Matches aeoTwins.freshLayer.type. */
7
+ type?: 'r2' | 'kv';
8
+ /** Retention in days. Matches aeoTwins.freshLayer.retentionDays. */
9
+ retentionDays?: number;
10
+ /** Optional structured-log sink. */
11
+ log?: (event: Record<string, unknown>) => void;
12
+ }
13
+ export interface PruneResult {
14
+ deploymentsScanned: number;
15
+ deploymentsPruned: string[];
16
+ keysDeleted: number;
17
+ errors: string[];
18
+ }
19
+ /**
20
+ * Sweep the fresh layer for entries belonging to deployments that are no longer
21
+ * current AND older than `retentionDays`. Keys from the current deployment are
22
+ * untouched regardless of age.
23
+ *
24
+ * Algorithm:
25
+ * 1. Determine the current versionId from env.CF_VERSION_METADATA.id.
26
+ * 2. List all deployment IDs present in the fresh layer.
27
+ * 3. For each non-current deployment, list its keys; if the oldest matches
28
+ * the retention cutoff, delete all keys under that prefix.
29
+ *
30
+ * "Oldest matches retention cutoff" is approximated by R2's `uploaded` timestamp
31
+ * (R2-only). For KV there's no per-key mtime, so the retention check degrades
32
+ * to "delete everything not matching the current deployment" — adjust the
33
+ * binding to R2 if you need retentionDays fidelity.
34
+ */
35
+ export declare function pruneAeoR2(options: PruneAeoR2Options): Promise<PruneResult>;
36
+ //# sourceMappingURL=prune-aeo-r2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune-aeo-r2.d.ts","sourceRoot":"","sources":["../../src/cron/prune-aeo-r2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAW5C,MAAM,WAAW,iBAAiB;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oCAAoC;IACpC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CAC9C;AAED,MAAM,WAAW,WAAW;IAC3B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,EAAE,CAAA;CAChB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAkFjF"}
@@ -0,0 +1,94 @@
1
+ import { deleteFreshTwin, listDeploymentIds, listKeysByDeployment } from '../utils/fresh-layer.js';
2
+ const DEFAULT_RETENTION_DAYS = 7;
3
+ /**
4
+ * Sweep the fresh layer for entries belonging to deployments that are no longer
5
+ * current AND older than `retentionDays`. Keys from the current deployment are
6
+ * untouched regardless of age.
7
+ *
8
+ * Algorithm:
9
+ * 1. Determine the current versionId from env.CF_VERSION_METADATA.id.
10
+ * 2. List all deployment IDs present in the fresh layer.
11
+ * 3. For each non-current deployment, list its keys; if the oldest matches
12
+ * the retention cutoff, delete all keys under that prefix.
13
+ *
14
+ * "Oldest matches retention cutoff" is approximated by R2's `uploaded` timestamp
15
+ * (R2-only). For KV there's no per-key mtime, so the retention check degrades
16
+ * to "delete everything not matching the current deployment" — adjust the
17
+ * binding to R2 if you need retentionDays fidelity.
18
+ */
19
+ export async function pruneAeoR2(options) {
20
+ const { env, bindingName = 'AEO_TWINS', type = 'r2', retentionDays = DEFAULT_RETENTION_DAYS, log, } = options;
21
+ const currentVersion = env.CF_VERSION_METADATA?.id;
22
+ const errors = [];
23
+ const deploymentsPruned = [];
24
+ let keysDeleted = 0;
25
+ if (!currentVersion) {
26
+ errors.push('missing_CF_VERSION_METADATA_id');
27
+ log?.({ event: 'prune_skipped', reason: 'missing_CF_VERSION_METADATA_id' });
28
+ return { deploymentsScanned: 0, deploymentsPruned, keysDeleted, errors };
29
+ }
30
+ const binding = env[bindingName];
31
+ if (!binding) {
32
+ errors.push(`missing_binding_${bindingName}`);
33
+ log?.({ event: 'prune_skipped', reason: 'missing_binding', bindingName });
34
+ return { deploymentsScanned: 0, deploymentsPruned, keysDeleted, errors };
35
+ }
36
+ const impl = binding;
37
+ const freshLayer = { type, impl, deploymentId: currentVersion };
38
+ const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
39
+ const allDeployments = await listDeploymentIds(freshLayer);
40
+ const deploymentsScanned = allDeployments.size;
41
+ for (const deploymentId of allDeployments) {
42
+ if (deploymentId === currentVersion)
43
+ continue;
44
+ let deletedForThisDeployment = 0;
45
+ let shouldDelete = true;
46
+ // R2-only: peek the first object's uploaded time to gate on retentionDays.
47
+ // KV falls through to "delete everything non-current" since no mtime is available.
48
+ if (type === 'r2') {
49
+ const r2 = impl;
50
+ const peek = await r2.list({ prefix: `twin/${deploymentId}/`, limit: 1 });
51
+ const oldest = peek.objects[0];
52
+ if (oldest && oldest.uploaded.getTime() > cutoffMs) {
53
+ // Inside retention window — skip.
54
+ shouldDelete = false;
55
+ log?.({
56
+ event: 'prune_skipped_within_retention',
57
+ deploymentId,
58
+ oldestUploaded: oldest.uploaded.toISOString(),
59
+ retentionDays,
60
+ });
61
+ }
62
+ }
63
+ if (!shouldDelete)
64
+ continue;
65
+ try {
66
+ for await (const key of listKeysByDeployment(freshLayer, deploymentId)) {
67
+ await deleteFreshTwin({ ...freshLayer, deploymentId }, keyToPath(deploymentId, key));
68
+ deletedForThisDeployment++;
69
+ }
70
+ keysDeleted += deletedForThisDeployment;
71
+ deploymentsPruned.push(deploymentId);
72
+ log?.({
73
+ event: 'prune_deployment',
74
+ deploymentId,
75
+ keysDeleted: deletedForThisDeployment,
76
+ });
77
+ }
78
+ catch (err) {
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ errors.push(`deployment_${deploymentId}: ${msg}`);
81
+ log?.({ event: 'prune_error', deploymentId, message: msg });
82
+ }
83
+ }
84
+ return { deploymentsScanned, deploymentsPruned, keysDeleted, errors };
85
+ }
86
+ /**
87
+ * Convert a full R2/KV key back to the URL-path form used by deleteFreshTwin.
88
+ * Inverse of fresh-layer buildKey.
89
+ */
90
+ function keyToPath(deploymentId, key) {
91
+ const prefix = `twin/${deploymentId}/`;
92
+ return key.startsWith(prefix) ? `/${key.slice(prefix.length)}` : `/${key}`;
93
+ }
94
+ //# sourceMappingURL=prune-aeo-r2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune-aeo-r2.js","sourceRoot":"","sources":["../../src/cron/prune-aeo-r2.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AA6BlG,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAEhC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAA0B;IAC1D,MAAM,EACL,GAAG,EACH,WAAW,GAAG,WAAW,EACzB,IAAI,GAAG,IAAI,EACX,aAAa,GAAG,sBAAsB,EACtC,GAAG,GACH,GAAG,OAAO,CAAA;IAEX,MAAM,cAAc,GAAG,GAAG,CAAC,mBAAmB,EAAE,EAAE,CAAA;IAClD,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,iBAAiB,GAAa,EAAE,CAAA;IACtC,IAAI,WAAW,GAAG,CAAC,CAAA;IAEnB,IAAI,CAAC,cAAc,EAAE,CAAC;QACrB,MAAM,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;QAC7C,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,gCAAgC,EAAE,CAAC,CAAA;QAC3E,OAAO,EAAE,kBAAkB,EAAE,CAAC,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,EAAE,CAAA;IACzE,CAAC;IAED,MAAM,OAAO,GAAI,GAA+B,CAAC,WAAW,CAAC,CAAA;IAC7D,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,CAAC,IAAI,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAA;QAC7C,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAC,CAAA;QACzE,OAAO,EAAE,kBAAkB,EAAE,CAAC,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,EAAE,CAAA;IACzE,CAAC;IAED,MAAM,IAAI,GAAG,OAE8B,CAAA;IAC3C,MAAM,UAAU,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,cAAc,EAAE,CAAA;IAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;IAEjE,MAAM,cAAc,GAAG,MAAM,iBAAiB,CAAC,UAAU,CAAC,CAAA;IAC1D,MAAM,kBAAkB,GAAG,cAAc,CAAC,IAAI,CAAA;IAE9C,KAAK,MAAM,YAAY,IAAI,cAAc,EAAE,CAAC;QAC3C,IAAI,YAAY,KAAK,cAAc;YAAE,SAAQ;QAE7C,IAAI,wBAAwB,GAAG,CAAC,CAAA;QAChC,IAAI,YAAY,GAAG,IAAI,CAAA;QAEvB,2EAA2E;QAC3E,mFAAmF;QACnF,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACnB,MAAM,EAAE,GAAG,IAA6C,CAAA;YACxD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,YAAY,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;YACzE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;YAC9B,IAAI,MAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC;gBACpD,kCAAkC;gBAClC,YAAY,GAAG,KAAK,CAAA;gBACpB,GAAG,EAAE,CAAC;oBACL,KAAK,EAAE,gCAAgC;oBACvC,YAAY;oBACZ,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE;oBAC7C,aAAa;iBACb,CAAC,CAAA;YACH,CAAC;QACF,CAAC;QAED,IAAI,CAAC,YAAY;YAAE,SAAQ;QAE3B,IAAI,CAAC;YACJ,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,oBAAoB,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,CAAC;gBACxE,MAAM,eAAe,CAAC,EAAE,GAAG,UAAU,EAAE,YAAY,EAAE,EAAE,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,CAAA;gBACpF,wBAAwB,EAAE,CAAA;YAC3B,CAAC;YACD,WAAW,IAAI,wBAAwB,CAAA;YACvC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YACpC,GAAG,EAAE,CAAC;gBACL,KAAK,EAAE,kBAAkB;gBACzB,YAAY;gBACZ,WAAW,EAAE,wBAAwB;aACrC,CAAC,CAAA;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAC5D,MAAM,CAAC,IAAI,CAAC,cAAc,YAAY,KAAK,GAAG,EAAE,CAAC,CAAA;YACjD,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;QAC5D,CAAC;IACF,CAAC;IAED,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,EAAE,CAAA;AACtE,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,YAAoB,EAAE,GAAW;IACnD,MAAM,MAAM,GAAG,QAAQ,YAAY,GAAG,CAAA;IACtC,OAAO,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAA;AAC3E,CAAC"}
@@ -0,0 +1,69 @@
1
+ export interface DurableObjectStorageLike {
2
+ get<T = unknown>(key: string): Promise<T | undefined>;
3
+ put<T = unknown>(key: string, value: T): Promise<void>;
4
+ delete(key: string): Promise<boolean>;
5
+ list<T = unknown>(options: {
6
+ prefix: string;
7
+ }): Promise<Map<string, T>>;
8
+ setAlarm(scheduledTime: number): Promise<void>;
9
+ getAlarm(): Promise<number | null>;
10
+ }
11
+ export interface DurableObjectStateLike {
12
+ storage: DurableObjectStorageLike;
13
+ }
14
+ export type CoordRequest = {
15
+ action: 'rate-check';
16
+ token: string;
17
+ limitRpm: number;
18
+ } | {
19
+ action: 'acquire-lock';
20
+ slug: string;
21
+ leaseMs?: number;
22
+ } | {
23
+ action: 'release-lock';
24
+ slug: string;
25
+ leaseId: string;
26
+ } | {
27
+ action: 'idempotency-check';
28
+ key: string;
29
+ } | {
30
+ action: 'idempotency-set';
31
+ key: string;
32
+ result: Record<string, unknown>;
33
+ ttlMs?: number;
34
+ };
35
+ export type CoordResponse = {
36
+ ok: true;
37
+ [k: string]: unknown;
38
+ } | {
39
+ ok: false;
40
+ error: string;
41
+ [k: string]: unknown;
42
+ };
43
+ export declare class AeoRevalidationCoordinator {
44
+ readonly ctx: DurableObjectStateLike;
45
+ constructor(ctx: DurableObjectStateLike);
46
+ /**
47
+ * Dispatch a JSON-encoded CoordRequest. Returns a JSON response. Not a
48
+ * public HTTP API — called from the consumer's Worker via the namespace
49
+ * binding: `await stub.fetch(new Request('https://internal/', { method: 'POST', body: JSON.stringify(req) }))`.
50
+ */
51
+ fetch(request: Request): Promise<Response>;
52
+ /**
53
+ * Alarm handler: runs every 5 minutes, sweeps expired entries across all
54
+ * three namespaces. Self-re-arming.
55
+ */
56
+ alarm(): Promise<void>;
57
+ private rateCheck;
58
+ private acquireLock;
59
+ private releaseLock;
60
+ private idempotencyCheck;
61
+ private idempotencySet;
62
+ private ensureAlarm;
63
+ }
64
+ export declare const _internals: {
65
+ LOCK_DEFAULT_LEASE_MS: number;
66
+ IDEMPOTENCY_DEFAULT_TTL_MS: number;
67
+ ALARM_INTERVAL_MS: number;
68
+ };
69
+ //# sourceMappingURL=aeo-revalidation-coord.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aeo-revalidation-coord.d.ts","sourceRoot":"","sources":["../../src/durable-objects/aeo-revalidation-coord.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,wBAAwB;IACxC,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IACrD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;IACvE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,wBAAwB,CAAA;CACjC;AAID,MAAM,MAAM,YAAY,GACrB;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,mBAAmB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9F,MAAM,MAAM,aAAa,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAA;AAyBrD,qBAAa,0BAA0B;IAC1B,QAAQ,CAAC,GAAG,EAAE,sBAAsB;gBAA3B,GAAG,EAAE,sBAAsB;IAEhD;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmChD;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA6Bd,SAAS;YAuBT,WAAW;YAaX,WAAW;YAaX,gBAAgB;YAShB,cAAc;YAad,WAAW;CAMzB;AAmBD,eAAO,MAAM,UAAU;;;;CAItB,CAAA"}