@growth-labs/seo 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +22 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +9 -5
- package/src/_internal/state.ts +26 -0
- package/src/bindings.ts +146 -0
- package/src/cron/prune-aeo-r2.ts +140 -0
- package/src/durable-objects/aeo-revalidation-coord.ts +246 -0
- package/src/index.ts +380 -0
- package/src/middleware/seo.ts +350 -0
- package/src/options.ts +456 -0
- package/src/routes/aeo-twin.ts +130 -0
- package/src/routes/apple-news.ts +36 -0
- package/src/routes/llms-full.ts +36 -0
- package/src/routes/llms.ts +15 -0
- package/src/routes/podcast-narration.ts +45 -0
- package/src/routes/podcast.ts +27 -0
- package/src/routes/revalidate.ts +298 -0
- package/src/routes/robots.ts +21 -0
- package/src/routes/rss.ts +29 -0
- package/src/routes/sitemap-articles.ts +25 -0
- package/src/routes/sitemap-index.ts +89 -0
- package/src/routes/sitemap-markdown.ts +39 -0
- package/src/routes/sitemap-pages.ts +24 -0
- package/src/routes/sitemap-products.ts +24 -0
- package/src/routes/sitemap-videos.ts +24 -0
- package/src/runtime.ts +17 -0
- package/src/site-url-core.ts +71 -0
- package/src/site-url.ts +21 -0
- package/src/types.ts +166 -0
- package/src/utils/aeo-summary.ts +176 -0
- package/src/utils/aeo-twin-emitter.ts +173 -0
- package/src/utils/aeo.ts +223 -0
- package/src/utils/apple-news-anf.ts +163 -0
- package/src/utils/apple-news-rss.ts +136 -0
- package/src/utils/content-filter.ts +87 -0
- package/src/utils/crawler-class.ts +155 -0
- package/src/utils/define-content-provider.ts +65 -0
- package/src/utils/effective-auth.ts +44 -0
- package/src/utils/fcrdns.ts +269 -0
- package/src/utils/fresh-layer.ts +175 -0
- package/src/utils/hreflang.ts +26 -0
- package/src/utils/index.ts +91 -0
- package/src/utils/json-ld/article.ts +120 -0
- package/src/utils/json-ld/audio.ts +32 -0
- package/src/utils/json-ld/breadcrumb.ts +28 -0
- package/src/utils/json-ld/faq.ts +18 -0
- package/src/utils/json-ld/howto.ts +23 -0
- package/src/utils/json-ld/index.ts +12 -0
- package/src/utils/json-ld/item-list.ts +26 -0
- package/src/utils/json-ld/organization.ts +42 -0
- package/src/utils/json-ld/person.ts +25 -0
- package/src/utils/json-ld/product.ts +155 -0
- package/src/utils/json-ld/video.ts +20 -0
- package/src/utils/json-ld/website.ts +27 -0
- package/src/utils/llms-full.ts +90 -0
- package/src/utils/llms.ts +45 -0
- package/src/utils/meta.ts +184 -0
- package/src/utils/podcast.ts +112 -0
- package/src/utils/robots.ts +47 -0
- package/src/utils/rss.ts +64 -0
- package/src/utils/seo-head.ts +81 -0
- package/src/utils/sitemap-markdown.ts +80 -0
- package/src/utils/sitemap.ts +169 -0
- package/src/utils/staleness.ts +61 -0
- package/src/utils/validation.ts +308 -0
- package/src/virtual.d.ts +8 -0
- package/src/vite-plugin.ts +66 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,CAqEhF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,CAqEhF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,gBAAgB,CAyF3F;AA4BD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,MAAM,WAAW,wBAAwB;IACxC,GAAG,EAAE,MAAM,CAAA;IACX,iBAAiB,EAAE;QAClB,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;QACZ,qBAAqB,EAAE,MAAM,CAAA;KAC7B,CAAA;CACD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,wBAAwB,EAAE,CAkC5F;AAID,MAAM,WAAW,mBAAmB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,SAAS,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAAC,EAC9C,eAAe,EACf,KAAK,GACL,EAAE;IACF,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAA;CACpB,GAAG,mBAAmB,EAAE,CAmBxB"}
|
package/dist/utils/validation.js
CHANGED
|
@@ -69,6 +69,9 @@ export function validateJsonLd(jsonLd) {
|
|
|
69
69
|
export function validatePage(html, options) {
|
|
70
70
|
const errors = [];
|
|
71
71
|
const warnings = [];
|
|
72
|
+
if (isNoindexMetaRefreshRedirect(html)) {
|
|
73
|
+
return { errors, warnings };
|
|
74
|
+
}
|
|
72
75
|
const { titleMaxLength, descriptionMaxLength } = options;
|
|
73
76
|
// Title checks
|
|
74
77
|
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
@@ -138,6 +141,25 @@ export function validatePage(html, options) {
|
|
|
138
141
|
}
|
|
139
142
|
return { errors, warnings };
|
|
140
143
|
}
|
|
144
|
+
function isNoindexMetaRefreshRedirect(html) {
|
|
145
|
+
const metaTags = html.match(/<meta\s+[^>]*>/gi) ?? [];
|
|
146
|
+
const hasRefresh = metaTags.some((tag) => getHtmlAttr(tag, 'http-equiv')?.toLowerCase() === 'refresh');
|
|
147
|
+
const hasNoindex = metaTags.some((tag) => {
|
|
148
|
+
if (getHtmlAttr(tag, 'name')?.toLowerCase() !== 'robots')
|
|
149
|
+
return false;
|
|
150
|
+
const content = getHtmlAttr(tag, 'content')?.toLowerCase() ?? '';
|
|
151
|
+
return content
|
|
152
|
+
.split(',')
|
|
153
|
+
.map((part) => part.trim())
|
|
154
|
+
.includes('noindex');
|
|
155
|
+
});
|
|
156
|
+
return hasRefresh && hasNoindex;
|
|
157
|
+
}
|
|
158
|
+
function getHtmlAttr(tag, name) {
|
|
159
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
160
|
+
const match = tag.match(new RegExp(`\\b${escapedName}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i'));
|
|
161
|
+
return match?.[1] ?? match?.[2] ?? match?.[3];
|
|
162
|
+
}
|
|
141
163
|
/**
|
|
142
164
|
* Verify that every `alternateLocales` entry has a reciprocal entry on the
|
|
143
165
|
* target side. For search engines, missing reciprocals are a hard error — Google
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-labs/seo",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -51,7 +51,11 @@
|
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"dist",
|
|
54
|
-
"src
|
|
54
|
+
"src/**/*.ts",
|
|
55
|
+
"src/**/*.astro",
|
|
56
|
+
"src/**/*.css",
|
|
57
|
+
"src/**/*.md",
|
|
58
|
+
"src/**/*.sql",
|
|
55
59
|
"README.md"
|
|
56
60
|
],
|
|
57
61
|
"publishConfig": {
|
|
@@ -59,16 +63,16 @@
|
|
|
59
63
|
"registry": "https://registry.npmjs.org/"
|
|
60
64
|
},
|
|
61
65
|
"peerDependencies": {
|
|
62
|
-
"astro": "^6.
|
|
66
|
+
"astro": "^6.1.10"
|
|
63
67
|
},
|
|
64
68
|
"dependencies": {
|
|
65
69
|
"zod": "^3.23.0"
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
|
68
72
|
"@types/node": "^25.5.0",
|
|
69
|
-
"astro": "^6.
|
|
73
|
+
"astro": "^6.1.10",
|
|
70
74
|
"typescript": "^5.7.0",
|
|
71
|
-
"vite": "^7.
|
|
75
|
+
"vite": "^7.3.2",
|
|
72
76
|
"vitest": "^3.0.0"
|
|
73
77
|
},
|
|
74
78
|
"scripts": {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResolvedSeoOptions } from '../options.js'
|
|
2
|
+
import type { ContentProvider } from '../types.js'
|
|
3
|
+
|
|
4
|
+
let _config: ResolvedSeoOptions | null = null
|
|
5
|
+
let _contentProvider: ContentProvider | undefined
|
|
6
|
+
|
|
7
|
+
export function _setConfig(config: ResolvedSeoOptions): void {
|
|
8
|
+
_config = config
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function _setContentProvider(provider: ContentProvider): void {
|
|
12
|
+
_contentProvider = provider
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getConfig(): ResolvedSeoOptions {
|
|
16
|
+
if (!_config) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'@growth-labs/seo: integration not initialized. Ensure seo() is added to your Astro config.',
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
return _config
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getContentProvider(): ContentProvider | undefined {
|
|
25
|
+
return _contentProvider
|
|
26
|
+
}
|
package/src/bindings.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
|
|
11
|
+
/**
|
|
12
|
+
* R2 bucket binding for the fresh-twin layer.
|
|
13
|
+
* Subset of the R2Bucket API we actually use.
|
|
14
|
+
*/
|
|
15
|
+
export interface R2BucketLike {
|
|
16
|
+
get(key: string): Promise<R2ObjectLike | null>
|
|
17
|
+
put(
|
|
18
|
+
key: string,
|
|
19
|
+
value: ReadableStream | ArrayBuffer | string,
|
|
20
|
+
options?: R2PutOptions,
|
|
21
|
+
): Promise<unknown>
|
|
22
|
+
delete(keys: string | string[]): Promise<void>
|
|
23
|
+
list(options?: { prefix?: string; cursor?: string; limit?: number }): Promise<R2ListResult>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface R2ObjectLike {
|
|
27
|
+
readonly key: string
|
|
28
|
+
readonly size: number
|
|
29
|
+
readonly uploaded: Date
|
|
30
|
+
readonly httpMetadata?: R2HTTPMetadata
|
|
31
|
+
readonly customMetadata?: Record<string, string>
|
|
32
|
+
text(): Promise<string>
|
|
33
|
+
arrayBuffer(): Promise<ArrayBuffer>
|
|
34
|
+
body: ReadableStream | null
|
|
35
|
+
bodyUsed: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface R2PutOptions {
|
|
39
|
+
httpMetadata?: R2HTTPMetadata
|
|
40
|
+
customMetadata?: Record<string, string>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface R2HTTPMetadata {
|
|
44
|
+
contentType?: string
|
|
45
|
+
contentLanguage?: string
|
|
46
|
+
contentDisposition?: string
|
|
47
|
+
contentEncoding?: string
|
|
48
|
+
cacheControl?: string
|
|
49
|
+
cacheExpiry?: Date
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface R2ListResult {
|
|
53
|
+
objects: R2ObjectLike[]
|
|
54
|
+
truncated: boolean
|
|
55
|
+
cursor?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* KV namespace binding — acceptable alternative to R2 for the fresh-twin layer.
|
|
60
|
+
* Subset of the KVNamespace API we actually use.
|
|
61
|
+
*/
|
|
62
|
+
export interface KVNamespaceLike {
|
|
63
|
+
get(key: string): Promise<string | null>
|
|
64
|
+
get(key: string, options: { type: 'text' }): Promise<string | null>
|
|
65
|
+
get(key: string, options: { type: 'arrayBuffer' }): Promise<ArrayBuffer | null>
|
|
66
|
+
put(
|
|
67
|
+
key: string,
|
|
68
|
+
value: string | ArrayBuffer,
|
|
69
|
+
options?: { expiration?: number; expirationTtl?: number; metadata?: unknown },
|
|
70
|
+
): Promise<void>
|
|
71
|
+
delete(key: string): Promise<void>
|
|
72
|
+
list(options?: {
|
|
73
|
+
prefix?: string
|
|
74
|
+
cursor?: string
|
|
75
|
+
limit?: number
|
|
76
|
+
}): Promise<{ keys: Array<{ name: string }>; list_complete: boolean; cursor?: string }>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Assets binding — reads static files from the Worker's deployment bundle without
|
|
81
|
+
* re-entering the Worker's request routing. Use:
|
|
82
|
+
* env.ASSETS.fetch(new Request('https://assets.local/path/to/file.md'))
|
|
83
|
+
* The hostname is ignored by the assets runtime; only pathname matters.
|
|
84
|
+
* Set `assets.not_found_handling = "none"` in wrangler.toml to prevent SPA fallback
|
|
85
|
+
* from serving index.html when the requested path is missing.
|
|
86
|
+
*/
|
|
87
|
+
export interface AssetsBinding {
|
|
88
|
+
fetch(request: Request): Promise<Response>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Durable Object namespace binding for the Revalidation Coordinator. The actual DO
|
|
93
|
+
* class is exported from `@growth-labs/seo/durable-objects` and wired by the consumer
|
|
94
|
+
* in their Worker entrypoint.
|
|
95
|
+
*/
|
|
96
|
+
export interface DurableObjectNamespaceLike {
|
|
97
|
+
idFromName(name: string): DurableObjectIdLike
|
|
98
|
+
get(id: DurableObjectIdLike): DurableObjectStubLike
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface DurableObjectIdLike {
|
|
102
|
+
toString(): string
|
|
103
|
+
equals(other: DurableObjectIdLike): boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DurableObjectStubLike {
|
|
107
|
+
fetch(request: Request): Promise<Response>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Version metadata binding. Injected into `env` as `CF_VERSION_METADATA`.
|
|
112
|
+
* Used for rollback-safe R2 key prefixing (`twin/<id>/<slug>.md`).
|
|
113
|
+
* `id` is an opaque version UUID that rotates on every new deployment.
|
|
114
|
+
* `tag` is an optional user-supplied annotation via `workers/tag`.
|
|
115
|
+
* `timestamp` is the version's creation time (ISO 8601 string).
|
|
116
|
+
* Ref: https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/
|
|
117
|
+
*/
|
|
118
|
+
export interface VersionMetadata {
|
|
119
|
+
readonly id: string
|
|
120
|
+
readonly tag?: string
|
|
121
|
+
readonly timestamp: string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The minimal Worker env shape the SEO package depends on. Consumers will have many
|
|
126
|
+
* more bindings than this; we only declare what we read.
|
|
127
|
+
*
|
|
128
|
+
* All fields are optional because the package's `mode: 'static'` code path runs without
|
|
129
|
+
* any Worker bindings. Runtime binding resolvers throw structured errors when required
|
|
130
|
+
* bindings are missing.
|
|
131
|
+
*/
|
|
132
|
+
export interface SeoEnv {
|
|
133
|
+
// Fresh-twin layer — one of R2 or KV. The package's resolver picks by binding shape;
|
|
134
|
+
// the `type` hint in `aeoTwins.freshLayer` avoids ambiguity.
|
|
135
|
+
AEO_TWINS?: R2BucketLike | KVNamespaceLike
|
|
136
|
+
|
|
137
|
+
// Revalidation Coordinator DO namespace.
|
|
138
|
+
AEO_REVALIDATION_COORD?: DurableObjectNamespaceLike
|
|
139
|
+
|
|
140
|
+
// Static assets bundled with the Worker deployment. Provided by Astro's Cloudflare
|
|
141
|
+
// adapter for SSR routes and by Workers' [assets] binding for static serving.
|
|
142
|
+
ASSETS?: AssetsBinding
|
|
143
|
+
|
|
144
|
+
// Version metadata. Required when `mode !== 'static'` or `onDemandRevalidation: true`.
|
|
145
|
+
CF_VERSION_METADATA?: VersionMetadata
|
|
146
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { SeoEnv } from '../bindings.js'
|
|
2
|
+
import { deleteFreshTwin, listDeploymentIds, listKeysByDeployment } from '../utils/fresh-layer.js'
|
|
3
|
+
|
|
4
|
+
// Scheduled cron handler that deletes R2/KV entries from old deployments past
|
|
5
|
+
// the configured retention. Consumer binds a cron trigger in wrangler.toml
|
|
6
|
+
// (typically daily at 03:00 UTC) and re-exports this function from their
|
|
7
|
+
// Worker's `scheduled` handler.
|
|
8
|
+
//
|
|
9
|
+
// R2 does not have native prefix-based TTL. Without this cron, old deployment
|
|
10
|
+
// prefixes accumulate — storage cost is low but unbounded.
|
|
11
|
+
|
|
12
|
+
export interface PruneAeoR2Options {
|
|
13
|
+
env: SeoEnv
|
|
14
|
+
/** Binding name to read from env. Matches aeoTwins.freshLayer.bindingName. */
|
|
15
|
+
bindingName?: string
|
|
16
|
+
/** 'r2' (default) or 'kv'. Matches aeoTwins.freshLayer.type. */
|
|
17
|
+
type?: 'r2' | 'kv'
|
|
18
|
+
/** Retention in days. Matches aeoTwins.freshLayer.retentionDays. */
|
|
19
|
+
retentionDays?: number
|
|
20
|
+
/** Optional structured-log sink. */
|
|
21
|
+
log?: (event: Record<string, unknown>) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PruneResult {
|
|
25
|
+
deploymentsScanned: number
|
|
26
|
+
deploymentsPruned: string[]
|
|
27
|
+
keysDeleted: number
|
|
28
|
+
errors: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_RETENTION_DAYS = 7
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sweep the fresh layer for entries belonging to deployments that are no longer
|
|
35
|
+
* current AND older than `retentionDays`. Keys from the current deployment are
|
|
36
|
+
* untouched regardless of age.
|
|
37
|
+
*
|
|
38
|
+
* Algorithm:
|
|
39
|
+
* 1. Determine the current versionId from env.CF_VERSION_METADATA.id.
|
|
40
|
+
* 2. List all deployment IDs present in the fresh layer.
|
|
41
|
+
* 3. For each non-current deployment, list its keys; if the oldest matches
|
|
42
|
+
* the retention cutoff, delete all keys under that prefix.
|
|
43
|
+
*
|
|
44
|
+
* "Oldest matches retention cutoff" is approximated by R2's `uploaded` timestamp
|
|
45
|
+
* (R2-only). For KV there's no per-key mtime, so the retention check degrades
|
|
46
|
+
* to "delete everything not matching the current deployment" — adjust the
|
|
47
|
+
* binding to R2 if you need retentionDays fidelity.
|
|
48
|
+
*/
|
|
49
|
+
export async function pruneAeoR2(options: PruneAeoR2Options): Promise<PruneResult> {
|
|
50
|
+
const {
|
|
51
|
+
env,
|
|
52
|
+
bindingName = 'AEO_TWINS',
|
|
53
|
+
type = 'r2',
|
|
54
|
+
retentionDays = DEFAULT_RETENTION_DAYS,
|
|
55
|
+
log,
|
|
56
|
+
} = options
|
|
57
|
+
|
|
58
|
+
const currentVersion = env.CF_VERSION_METADATA?.id
|
|
59
|
+
const errors: string[] = []
|
|
60
|
+
const deploymentsPruned: string[] = []
|
|
61
|
+
let keysDeleted = 0
|
|
62
|
+
|
|
63
|
+
if (!currentVersion) {
|
|
64
|
+
errors.push('missing_CF_VERSION_METADATA_id')
|
|
65
|
+
log?.({ event: 'prune_skipped', reason: 'missing_CF_VERSION_METADATA_id' })
|
|
66
|
+
return { deploymentsScanned: 0, deploymentsPruned, keysDeleted, errors }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const binding = (env as Record<string, unknown>)[bindingName]
|
|
70
|
+
if (!binding) {
|
|
71
|
+
errors.push(`missing_binding_${bindingName}`)
|
|
72
|
+
log?.({ event: 'prune_skipped', reason: 'missing_binding', bindingName })
|
|
73
|
+
return { deploymentsScanned: 0, deploymentsPruned, keysDeleted, errors }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const impl = binding as
|
|
77
|
+
| import('../bindings.js').R2BucketLike
|
|
78
|
+
| import('../bindings.js').KVNamespaceLike
|
|
79
|
+
const freshLayer = { type, impl, deploymentId: currentVersion }
|
|
80
|
+
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000
|
|
81
|
+
|
|
82
|
+
const allDeployments = await listDeploymentIds(freshLayer)
|
|
83
|
+
const deploymentsScanned = allDeployments.size
|
|
84
|
+
|
|
85
|
+
for (const deploymentId of allDeployments) {
|
|
86
|
+
if (deploymentId === currentVersion) continue
|
|
87
|
+
|
|
88
|
+
let deletedForThisDeployment = 0
|
|
89
|
+
let shouldDelete = true
|
|
90
|
+
|
|
91
|
+
// R2-only: peek the first object's uploaded time to gate on retentionDays.
|
|
92
|
+
// KV falls through to "delete everything non-current" since no mtime is available.
|
|
93
|
+
if (type === 'r2') {
|
|
94
|
+
const r2 = impl as import('../bindings.js').R2BucketLike
|
|
95
|
+
const peek = await r2.list({ prefix: `twin/${deploymentId}/`, limit: 1 })
|
|
96
|
+
const oldest = peek.objects[0]
|
|
97
|
+
if (oldest && oldest.uploaded.getTime() > cutoffMs) {
|
|
98
|
+
// Inside retention window — skip.
|
|
99
|
+
shouldDelete = false
|
|
100
|
+
log?.({
|
|
101
|
+
event: 'prune_skipped_within_retention',
|
|
102
|
+
deploymentId,
|
|
103
|
+
oldestUploaded: oldest.uploaded.toISOString(),
|
|
104
|
+
retentionDays,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!shouldDelete) continue
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
for await (const key of listKeysByDeployment(freshLayer, deploymentId)) {
|
|
113
|
+
await deleteFreshTwin({ ...freshLayer, deploymentId }, keyToPath(deploymentId, key))
|
|
114
|
+
deletedForThisDeployment++
|
|
115
|
+
}
|
|
116
|
+
keysDeleted += deletedForThisDeployment
|
|
117
|
+
deploymentsPruned.push(deploymentId)
|
|
118
|
+
log?.({
|
|
119
|
+
event: 'prune_deployment',
|
|
120
|
+
deploymentId,
|
|
121
|
+
keysDeleted: deletedForThisDeployment,
|
|
122
|
+
})
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
125
|
+
errors.push(`deployment_${deploymentId}: ${msg}`)
|
|
126
|
+
log?.({ event: 'prune_error', deploymentId, message: msg })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { deploymentsScanned, deploymentsPruned, keysDeleted, errors }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convert a full R2/KV key back to the URL-path form used by deleteFreshTwin.
|
|
135
|
+
* Inverse of fresh-layer buildKey.
|
|
136
|
+
*/
|
|
137
|
+
function keyToPath(deploymentId: string, key: string): string {
|
|
138
|
+
const prefix = `twin/${deploymentId}/`
|
|
139
|
+
return key.startsWith(prefix) ? `/${key.slice(prefix.length)}` : `/${key}`
|
|
140
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Revalidation Coordinator — a single Durable Object class that manages three
|
|
2
|
+
// concerns for the revalidation endpoint and middleware fallthrough path:
|
|
3
|
+
//
|
|
4
|
+
// rl:<token> — token-bucket rate-limit state (10 RPM per token)
|
|
5
|
+
// lock:<slug> — per-slug advisory lock (30s max hold)
|
|
6
|
+
// idempotency:<key> — response dedup cache (10-minute TTL)
|
|
7
|
+
//
|
|
8
|
+
// One DO instance per request-hostname (derived from url.host at request time,
|
|
9
|
+
// NOT build-time options.site). Multi-tenant Workers serving multiple domains
|
|
10
|
+
// from one deployment get per-tenant isolation.
|
|
11
|
+
//
|
|
12
|
+
// Uses the async KV API (ctx.storage.get/put/delete/list) on the SQLite-backed
|
|
13
|
+
// DO runtime — no raw SQL. TTL eviction runs via a self-scheduled 5-minute
|
|
14
|
+
// alarm; if an alarm misses (Worker restart), the next write re-arms it.
|
|
15
|
+
//
|
|
16
|
+
// Export the class from `@growth-labs/seo/durable-objects`; consumers re-export
|
|
17
|
+
// from their Worker entrypoint per Cloudflare DO conventions.
|
|
18
|
+
|
|
19
|
+
// ─── Structural types ───
|
|
20
|
+
|
|
21
|
+
export interface DurableObjectStorageLike {
|
|
22
|
+
get<T = unknown>(key: string): Promise<T | undefined>
|
|
23
|
+
put<T = unknown>(key: string, value: T): Promise<void>
|
|
24
|
+
delete(key: string): Promise<boolean>
|
|
25
|
+
list<T = unknown>(options: { prefix: string }): Promise<Map<string, T>>
|
|
26
|
+
setAlarm(scheduledTime: number): Promise<void>
|
|
27
|
+
getAlarm(): Promise<number | null>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DurableObjectStateLike {
|
|
31
|
+
storage: DurableObjectStorageLike
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Wire protocol ───
|
|
35
|
+
|
|
36
|
+
export type CoordRequest =
|
|
37
|
+
| { action: 'rate-check'; token: string; limitRpm: number }
|
|
38
|
+
| { action: 'acquire-lock'; slug: string; leaseMs?: number }
|
|
39
|
+
| { action: 'release-lock'; slug: string; leaseId: string }
|
|
40
|
+
| { action: 'idempotency-check'; key: string }
|
|
41
|
+
| { action: 'idempotency-set'; key: string; result: Record<string, unknown>; ttlMs?: number }
|
|
42
|
+
|
|
43
|
+
export type CoordResponse =
|
|
44
|
+
| { ok: true; [k: string]: unknown }
|
|
45
|
+
| { ok: false; error: string; [k: string]: unknown }
|
|
46
|
+
|
|
47
|
+
interface RateLimitEntry {
|
|
48
|
+
tokens: number
|
|
49
|
+
lastRefill: number // epoch ms
|
|
50
|
+
capacity: number // RPM
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface LockEntry {
|
|
54
|
+
leaseId: string
|
|
55
|
+
expiresAt: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface IdempotencyEntry {
|
|
59
|
+
result: Record<string, unknown>
|
|
60
|
+
expiresAt: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const LOCK_DEFAULT_LEASE_MS = 30_000
|
|
64
|
+
const IDEMPOTENCY_DEFAULT_TTL_MS = 10 * 60 * 1000
|
|
65
|
+
const ALARM_INTERVAL_MS = 5 * 60 * 1000
|
|
66
|
+
const RATE_LIMIT_MAX_IDLE_MS = 10 * 60 * 1000
|
|
67
|
+
|
|
68
|
+
// ─── DO class ───
|
|
69
|
+
|
|
70
|
+
export class AeoRevalidationCoordinator {
|
|
71
|
+
constructor(readonly ctx: DurableObjectStateLike) {}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Dispatch a JSON-encoded CoordRequest. Returns a JSON response. Not a
|
|
75
|
+
* public HTTP API — called from the consumer's Worker via the namespace
|
|
76
|
+
* binding: `await stub.fetch(new Request('https://internal/', { method: 'POST', body: JSON.stringify(req) }))`.
|
|
77
|
+
*/
|
|
78
|
+
async fetch(request: Request): Promise<Response> {
|
|
79
|
+
if (request.method !== 'POST') {
|
|
80
|
+
return json({ ok: false, error: 'method_not_allowed' }, 405)
|
|
81
|
+
}
|
|
82
|
+
let body: CoordRequest
|
|
83
|
+
try {
|
|
84
|
+
body = (await request.json()) as CoordRequest
|
|
85
|
+
} catch {
|
|
86
|
+
return json({ ok: false, error: 'bad_json' }, 400)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await this.ensureAlarm()
|
|
90
|
+
|
|
91
|
+
switch (body.action) {
|
|
92
|
+
case 'rate-check':
|
|
93
|
+
return json(await this.rateCheck(body.token, body.limitRpm))
|
|
94
|
+
case 'acquire-lock':
|
|
95
|
+
return json(await this.acquireLock(body.slug, body.leaseMs ?? LOCK_DEFAULT_LEASE_MS))
|
|
96
|
+
case 'release-lock':
|
|
97
|
+
return json(await this.releaseLock(body.slug, body.leaseId))
|
|
98
|
+
case 'idempotency-check':
|
|
99
|
+
return json(await this.idempotencyCheck(body.key))
|
|
100
|
+
case 'idempotency-set':
|
|
101
|
+
return json(
|
|
102
|
+
await this.idempotencySet(
|
|
103
|
+
body.key,
|
|
104
|
+
body.result,
|
|
105
|
+
body.ttlMs ?? IDEMPOTENCY_DEFAULT_TTL_MS,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
default:
|
|
109
|
+
return json({ ok: false, error: 'unknown_action' }, 400)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Alarm handler: runs every 5 minutes, sweeps expired entries across all
|
|
115
|
+
* three namespaces. Self-re-arming.
|
|
116
|
+
*/
|
|
117
|
+
async alarm(): Promise<void> {
|
|
118
|
+
const now = Date.now()
|
|
119
|
+
|
|
120
|
+
// rate-limit: delete token buckets that haven't been refilled in >10 min.
|
|
121
|
+
const rlEntries = await this.ctx.storage.list<RateLimitEntry>({ prefix: 'rl:' })
|
|
122
|
+
for (const [key, entry] of rlEntries) {
|
|
123
|
+
if (now - entry.lastRefill > RATE_LIMIT_MAX_IDLE_MS) {
|
|
124
|
+
await this.ctx.storage.delete(key)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// lock: delete expired leases.
|
|
129
|
+
const lockEntries = await this.ctx.storage.list<LockEntry>({ prefix: 'lock:' })
|
|
130
|
+
for (const [key, entry] of lockEntries) {
|
|
131
|
+
if (now >= entry.expiresAt) await this.ctx.storage.delete(key)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// idempotency: delete expired entries.
|
|
135
|
+
const idemEntries = await this.ctx.storage.list<IdempotencyEntry>({ prefix: 'idempotency:' })
|
|
136
|
+
for (const [key, entry] of idemEntries) {
|
|
137
|
+
if (now >= entry.expiresAt) await this.ctx.storage.delete(key)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Re-arm the alarm.
|
|
141
|
+
await this.ctx.storage.setAlarm(now + ALARM_INTERVAL_MS)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Rate limit ───
|
|
145
|
+
|
|
146
|
+
private async rateCheck(token: string, limitRpm: number): Promise<CoordResponse> {
|
|
147
|
+
const key = `rl:${token}`
|
|
148
|
+
const now = Date.now()
|
|
149
|
+
const existing = await this.ctx.storage.get<RateLimitEntry>(key)
|
|
150
|
+
const refillRate = limitRpm / 60_000 // tokens per ms
|
|
151
|
+
let tokens: number
|
|
152
|
+
if (existing) {
|
|
153
|
+
const elapsed = now - existing.lastRefill
|
|
154
|
+
tokens = Math.min(limitRpm, existing.tokens + elapsed * refillRate)
|
|
155
|
+
} else {
|
|
156
|
+
tokens = limitRpm
|
|
157
|
+
}
|
|
158
|
+
if (tokens < 1) {
|
|
159
|
+
const retryAfterMs = Math.ceil((1 - tokens) / refillRate)
|
|
160
|
+
return { ok: false, error: 'rate_limited', retryAfterMs }
|
|
161
|
+
}
|
|
162
|
+
const next: RateLimitEntry = { tokens: tokens - 1, lastRefill: now, capacity: limitRpm }
|
|
163
|
+
await this.ctx.storage.put(key, next)
|
|
164
|
+
return { ok: true, remaining: Math.floor(next.tokens) }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Per-slug lock ───
|
|
168
|
+
|
|
169
|
+
private async acquireLock(slug: string, leaseMs: number): Promise<CoordResponse> {
|
|
170
|
+
const key = `lock:${slug}`
|
|
171
|
+
const now = Date.now()
|
|
172
|
+
const existing = await this.ctx.storage.get<LockEntry>(key)
|
|
173
|
+
if (existing && now < existing.expiresAt) {
|
|
174
|
+
return { ok: false, error: 'locked', expiresAt: existing.expiresAt }
|
|
175
|
+
}
|
|
176
|
+
const leaseId = newLeaseId()
|
|
177
|
+
const entry: LockEntry = { leaseId, expiresAt: now + leaseMs }
|
|
178
|
+
await this.ctx.storage.put(key, entry)
|
|
179
|
+
return { ok: true, leaseId, expiresAt: entry.expiresAt }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async releaseLock(slug: string, leaseId: string): Promise<CoordResponse> {
|
|
183
|
+
const key = `lock:${slug}`
|
|
184
|
+
const existing = await this.ctx.storage.get<LockEntry>(key)
|
|
185
|
+
if (!existing) return { ok: true, note: 'not_held' }
|
|
186
|
+
if (existing.leaseId !== leaseId) {
|
|
187
|
+
return { ok: false, error: 'lease_mismatch' }
|
|
188
|
+
}
|
|
189
|
+
await this.ctx.storage.delete(key)
|
|
190
|
+
return { ok: true }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Idempotency ───
|
|
194
|
+
|
|
195
|
+
private async idempotencyCheck(key: string): Promise<CoordResponse> {
|
|
196
|
+
const storageKey = `idempotency:${key}`
|
|
197
|
+
const entry = await this.ctx.storage.get<IdempotencyEntry>(storageKey)
|
|
198
|
+
if (!entry || Date.now() >= entry.expiresAt) {
|
|
199
|
+
return { ok: true, hit: false }
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, hit: true, result: entry.result }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async idempotencySet(
|
|
205
|
+
key: string,
|
|
206
|
+
result: Record<string, unknown>,
|
|
207
|
+
ttlMs: number,
|
|
208
|
+
): Promise<CoordResponse> {
|
|
209
|
+
const storageKey = `idempotency:${key}`
|
|
210
|
+
const entry: IdempotencyEntry = { result, expiresAt: Date.now() + ttlMs }
|
|
211
|
+
await this.ctx.storage.put(storageKey, entry)
|
|
212
|
+
return { ok: true }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Alarm management ───
|
|
216
|
+
|
|
217
|
+
private async ensureAlarm(): Promise<void> {
|
|
218
|
+
const current = await this.ctx.storage.getAlarm()
|
|
219
|
+
if (current === null) {
|
|
220
|
+
await this.ctx.storage.setAlarm(Date.now() + ALARM_INTERVAL_MS)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Helpers ───
|
|
226
|
+
|
|
227
|
+
function json(body: CoordResponse, status = 200): Response {
|
|
228
|
+
return new Response(JSON.stringify(body), {
|
|
229
|
+
status,
|
|
230
|
+
headers: { 'content-type': 'application/json' },
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function newLeaseId(): string {
|
|
235
|
+
const bytes = new Uint8Array(16)
|
|
236
|
+
crypto.getRandomValues(bytes)
|
|
237
|
+
let hex = ''
|
|
238
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, '0')
|
|
239
|
+
return hex
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const _internals = {
|
|
243
|
+
LOCK_DEFAULT_LEASE_MS,
|
|
244
|
+
IDEMPOTENCY_DEFAULT_TTL_MS,
|
|
245
|
+
ALARM_INTERVAL_MS,
|
|
246
|
+
}
|