@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.
Files changed (68) hide show
  1. package/dist/utils/validation.d.ts.map +1 -1
  2. package/dist/utils/validation.js +22 -0
  3. package/dist/utils/validation.js.map +1 -1
  4. package/package.json +9 -5
  5. package/src/_internal/state.ts +26 -0
  6. package/src/bindings.ts +146 -0
  7. package/src/cron/prune-aeo-r2.ts +140 -0
  8. package/src/durable-objects/aeo-revalidation-coord.ts +246 -0
  9. package/src/index.ts +380 -0
  10. package/src/middleware/seo.ts +350 -0
  11. package/src/options.ts +456 -0
  12. package/src/routes/aeo-twin.ts +130 -0
  13. package/src/routes/apple-news.ts +36 -0
  14. package/src/routes/llms-full.ts +36 -0
  15. package/src/routes/llms.ts +15 -0
  16. package/src/routes/podcast-narration.ts +45 -0
  17. package/src/routes/podcast.ts +27 -0
  18. package/src/routes/revalidate.ts +298 -0
  19. package/src/routes/robots.ts +21 -0
  20. package/src/routes/rss.ts +29 -0
  21. package/src/routes/sitemap-articles.ts +25 -0
  22. package/src/routes/sitemap-index.ts +89 -0
  23. package/src/routes/sitemap-markdown.ts +39 -0
  24. package/src/routes/sitemap-pages.ts +24 -0
  25. package/src/routes/sitemap-products.ts +24 -0
  26. package/src/routes/sitemap-videos.ts +24 -0
  27. package/src/runtime.ts +17 -0
  28. package/src/site-url-core.ts +71 -0
  29. package/src/site-url.ts +21 -0
  30. package/src/types.ts +166 -0
  31. package/src/utils/aeo-summary.ts +176 -0
  32. package/src/utils/aeo-twin-emitter.ts +173 -0
  33. package/src/utils/aeo.ts +223 -0
  34. package/src/utils/apple-news-anf.ts +163 -0
  35. package/src/utils/apple-news-rss.ts +136 -0
  36. package/src/utils/content-filter.ts +87 -0
  37. package/src/utils/crawler-class.ts +155 -0
  38. package/src/utils/define-content-provider.ts +65 -0
  39. package/src/utils/effective-auth.ts +44 -0
  40. package/src/utils/fcrdns.ts +269 -0
  41. package/src/utils/fresh-layer.ts +175 -0
  42. package/src/utils/hreflang.ts +26 -0
  43. package/src/utils/index.ts +91 -0
  44. package/src/utils/json-ld/article.ts +120 -0
  45. package/src/utils/json-ld/audio.ts +32 -0
  46. package/src/utils/json-ld/breadcrumb.ts +28 -0
  47. package/src/utils/json-ld/faq.ts +18 -0
  48. package/src/utils/json-ld/howto.ts +23 -0
  49. package/src/utils/json-ld/index.ts +12 -0
  50. package/src/utils/json-ld/item-list.ts +26 -0
  51. package/src/utils/json-ld/organization.ts +42 -0
  52. package/src/utils/json-ld/person.ts +25 -0
  53. package/src/utils/json-ld/product.ts +155 -0
  54. package/src/utils/json-ld/video.ts +20 -0
  55. package/src/utils/json-ld/website.ts +27 -0
  56. package/src/utils/llms-full.ts +90 -0
  57. package/src/utils/llms.ts +45 -0
  58. package/src/utils/meta.ts +184 -0
  59. package/src/utils/podcast.ts +112 -0
  60. package/src/utils/robots.ts +47 -0
  61. package/src/utils/rss.ts +64 -0
  62. package/src/utils/seo-head.ts +81 -0
  63. package/src/utils/sitemap-markdown.ts +80 -0
  64. package/src/utils/sitemap.ts +169 -0
  65. package/src/utils/staleness.ts +61 -0
  66. package/src/utils/validation.ts +308 -0
  67. package/src/virtual.d.ts +8 -0
  68. 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,CAqF3F;AAID,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"}
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"}
@@ -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":"AAWA;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,MAA+B;IAC7D,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,QAAQ,GAAa,EAAE,CAAA;IAE7B,qBAAqB;IACrB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACxC,CAAC;IAED,kBAAkB;IAClB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;IAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAA;IACrC,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;IAElE,iBAAiB;IACjB,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,aAAa,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;QACrF,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,+CAA+C,CAAC,CAAA;QACzE,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,gDAAgD,CAAC,CAAA;QAC1E,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;QAChC,IAAI,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACvE,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,+CAA+C,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAA;QAC3F,CAAC;IACF,CAAC;IAED,iBAAiB;IACjB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;QAC5B,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,QAAQ,CAAC,IAAI,CAAC,uDAAuD,CAAC,CAAA;QACvE,CAAC;aAAM,CAAC;YACP,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;YAC5D,IAAI,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;gBAChD,MAAM,CAAC,GAAG,SAAoC,CAAA;gBAC9C,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;oBAC/B,QAAQ,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAA;gBACxD,CAAC;gBACD,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC;oBACrB,QAAQ,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAA;gBAC/D,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IAED,mCAAmC;IACnC,IAAI,OAAO,KAAK,gBAAgB,EAAE,CAAC;QAClC,MAAM,eAAe,GAAG,MAAM,CAAC,eAAe,CAAA;QAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,CAAC;YACpC,IAAI,YAAY,GAAG,CAAC,CAAA;YACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACjD,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAA4B,CAAA;gBAC1D,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;gBACtC,IAAI,QAAQ,IAAI,YAAY,EAAE,CAAC;oBAC9B,MAAM,CAAC,IAAI,CACV,kCAAkC,CAAC,cAAc,QAAQ,4BAA4B,CACrF,CAAA;gBACF,CAAC;gBACD,YAAY,GAAG,QAAQ,CAAA;YACxB,CAAC;QACF,CAAC;IACF,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AAC5B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAA8B;IACxE,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,QAAQ,GAAa,EAAE,CAAA;IAE7B,MAAM,EAAE,cAAc,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAA;IAExD,eAAe;IACf,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACjE,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;IACnC,CAAC;SAAM,CAAC;QACP,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAClC,IAAI,KAAK,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CAAC,mBAAmB,cAAc,gBAAgB,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;QAChF,CAAC;IACF,CAAC;IAED,0BAA0B;IAC1B,MAAM,SAAS,GACd,IAAI,CAAC,KAAK,CAAC,6CAA6C,CAAC;QACzD,IAAI,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAA;IAClD,IAAI,CAAC,SAAS,EAAE,CAAC;QAChB,QAAQ,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IAC1C,CAAC;SAAM,CAAC;QACP,MAAM,YAAY,GACjB,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,2BAA2B,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAA;QAC5F,IAAI,YAAY,EAAE,CAAC;YAClB,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YACnC,IAAI,IAAI,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;gBACxC,QAAQ,CAAC,IAAI,CACZ,4BAA4B,oBAAoB,gBAAgB,IAAI,CAAC,MAAM,GAAG,CAC9E,CAAA;YACF,CAAC;QACF,CAAC;IACF,CAAC;IAED,kBAAkB;IAClB,MAAM,cAAc,GACnB,IAAI,CAAC,KAAK,CAAC,0CAA0C,CAAC;QACtD,IAAI,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAA;IAC/C,IAAI,CAAC,cAAc,EAAE,CAAC;QACrB,QAAQ,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;IACxC,CAAC;IAED,iBAAiB;IACjB,MAAM,YAAY,GACjB,IAAI,CAAC,KAAK,CAAC,8CAA8C,CAAC;QAC1D,IAAI,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAA;IACnD,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,QAAQ,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAA;IAC3C,CAAC;IAED,4CAA4C;IAC5C,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QAC1B,MAAM,YAAY,GACjB,IAAI,CAAC,KAAK,CAAC,oDAAoD,CAAC;YAChE,IAAI,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAA;QACzD,IAAI,YAAY,EAAE,CAAC;YAClB,MAAM,iBAAiB,GACtB,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,2BAA2B,CAAC;gBAClD,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAA;YAC5C,IAAI,iBAAiB,EAAE,CAAC;gBACvB,MAAM,KAAK,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAA;gBAC1C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;oBAC1D,QAAQ,CAAC,IAAI,CAAC,oBAAoB,KAAK,uBAAuB,OAAO,CAAC,YAAY,IAAI,CAAC,CAAA;gBACxF,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IAED,YAAY;IACZ,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAA;IAChD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IAChC,CAAC;SAAM,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,QAAQ,CAAC,IAAI,CAAC,2BAA2B,SAAS,CAAC,MAAM,GAAG,CAAC,CAAA;IAC9D,CAAC;IAED,yBAAyB;IACzB,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAA;IACzF,IAAI,CAAC,WAAW,EAAE,CAAC;QAClB,QAAQ,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAA;IACjD,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AAC5B,CAAC;AAeD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,2BAA2B,CAAC,KAAoB;IAC/D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC5C,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IAEnD,MAAM,MAAM,GAA+B,EAAE,CAAA;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC;YAAE,SAAQ;QAC1E,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YAClC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,CAAC;oBACX,GAAG,EAAE,IAAI,CAAC,GAAG;oBACb,iBAAiB,EAAE;wBAClB,IAAI,EAAE,GAAG,CAAC,GAAG;wBACb,IAAI,EAAE,GAAG,CAAC,IAAI;wBACd,qBAAqB,EAAE,IAAI,CAAC,GAAG;qBAC/B;iBACD,CAAC,CAAA;gBACF,SAAQ;YACT,CAAC;YACD,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC,CAAA;YAC5E,IAAI,CAAC,UAAU,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC;oBACX,GAAG,EAAE,IAAI,CAAC,GAAG;oBACb,iBAAiB,EAAE;wBAClB,IAAI,EAAE,GAAG,CAAC,GAAG;wBACb,IAAI,EAAE,GAAG,CAAC,IAAI;wBACd,qBAAqB,EAAE,IAAI,CAAC,GAAG;qBAC/B;iBACD,CAAC,CAAA;YACH,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAA;AACd,CAAC;AAUD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,8BAA8B,CAAC,EAC9C,eAAe,EACf,KAAK,GAIL;IACA,MAAM,MAAM,GAA0B,EAAE,CAAA;IACxC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,SAAQ;QACvC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;QACvC,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC;YAClE,MAAM,CAAC,IAAI,CAAC;gBACX,KAAK,EAAE,IAAI;gBACX,MAAM,EAAE,SAAS;gBACjB,OAAO,EACN,SAAS,IAAI,mDAAmD;oBAChE,uEAAuE;oBACvE,yEAAyE;oBACzE,0EAA0E;oBAC1E,oDAAoD;aACrD,CAAC,CAAA;QACH,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAA;AACd,CAAC"}
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.0",
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/components",
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.0.0"
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.0.0",
73
+ "astro": "^6.1.10",
70
74
  "typescript": "^5.7.0",
71
- "vite": "^7.0.0",
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
+ }
@@ -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
+ }