@lobehub/edge-contract 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -5,6 +5,7 @@ The gateway, landing, lobehub-cloud and lobe-chat import it instead of carrying
5
5
 
6
6
  | Subpath | What it owns |
7
7
  | --- | --- |
8
+ | `./cache-tags` | Business cache tags, raw-identifier builders, URL-segment decoding and canonical detail-tag recognition. |
8
9
  | `./landing-locales` | The URL path prefixes landing serves, and the default (`en`, at the bare path). |
9
10
  | `./seo-topics` | The discover topics landing server-renders for `?q=`. |
10
11
  | `./request-locale` | The app locale list and how a request picks one (`hl`, then the `LOBE_LOCALE` cookie, then Accept-Language). |
@@ -17,4 +18,31 @@ change every consumer will see, so update the case on purpose, in the same commi
17
18
 
18
19
  Design: `lobehub-gateway/docs/ryugi/specs/2026-11-06-logic-dedupe-design.md`.
19
20
 
21
+ ## Cache tag identities
22
+
23
+ Import from `@lobehub/edge-contract/cache-tags`. `marketplaceItemTag(kind, identifier)`
24
+ and `blogPostTag(slug)` take **raw** identifiers and encode them once. Invalid values
25
+ (empty, over 255 UTF-16 code units, ASCII controls or unpaired surrogates) return
26
+ `undefined`.
27
+
28
+ For a URL path segment, call `decodeCacheTagPathSegment` first. It decodes once and
29
+ returns the validated raw identifier, or `undefined` for malformed input:
30
+
31
+ ```ts
32
+ marketplaceItemTag('skill', 'owner/repo');
33
+ // landing:marketplace:skill:item:owner%2Frepo
34
+ marketplaceItemTag('skill', 'owner%2Frepo');
35
+ // landing:marketplace:skill:item:owner%252Frepo
36
+
37
+ const identifier = decodeCacheTagPathSegment('owner%2Frepo');
38
+ const tag = identifier === undefined ? undefined : marketplaceItemTag('skill', identifier);
39
+ // landing:marketplace:skill:item:owner%2Frepo
40
+ ```
41
+
42
+ `isCanonicalBlogPostTag` and `isCanonicalMarketplaceItemTag` recognize complete,
43
+ canonically encoded detail tags. They do not grant purge permission: entrypoint,
44
+ deployment, token and environment policy stay with the consumer. URL-to-tag matching,
45
+ origin hosts, Cloudflare calls and the gateway's `torii:*` cache identities also stay
46
+ in the gateway.
47
+
20
48
  Releases: bump `version`, push a `v<version>` tag; CI publishes to npm.
@@ -0,0 +1,19 @@
1
+ /** Business cache identities shared by response tagging and purge callers. */
2
+ export declare const LANDING_ALL_TAG = "landing:all";
3
+ export declare const BLOG_SECTION_TAG = "landing:blog";
4
+ export declare const BLOG_LIST_TAG = "landing:blog:list";
5
+ export declare const PRICING_TAG = "landing:pricing";
6
+ export declare const MARKETPLACE_KINDS: readonly ["mcp", "skill"];
7
+ export type MarketplaceKind = (typeof MARKETPLACE_KINDS)[number];
8
+ /** Decode a URL path segment once. Builders always take the resulting raw value. */
9
+ export declare const decodeCacheTagPathSegment: (segment: string) => string | undefined;
10
+ export declare const deploymentTag: (deployment: string) => string;
11
+ export declare const marketplaceListTag: (kind: MarketplaceKind) => string;
12
+ /** The identifier is raw: a literal `%2F` is distinct from a slash. */
13
+ export declare const marketplaceItemTag: (kind: MarketplaceKind, identifier: string) => string | undefined;
14
+ /** The slug is raw, including any literal percent characters. */
15
+ export declare const blogPostTag: (slug: string) => string | undefined;
16
+ /** Format recognition only; entrypoint and purge permissions belong to the caller. */
17
+ export declare const isCanonicalBlogPostTag: (tag: string) => boolean;
18
+ /** Recognize only canonical detail tags for a supported marketplace kind. */
19
+ export declare const isCanonicalMarketplaceItemTag: (tag: string) => boolean;
@@ -0,0 +1,56 @@
1
+ /** Business cache identities shared by response tagging and purge callers. */
2
+ export const LANDING_ALL_TAG = 'landing:all';
3
+ export const BLOG_SECTION_TAG = 'landing:blog';
4
+ export const BLOG_LIST_TAG = 'landing:blog:list';
5
+ export const PRICING_TAG = 'landing:pricing';
6
+ export const MARKETPLACE_KINDS = ['mcp', 'skill'];
7
+ const BLOG_POST_PREFIX = 'landing:blog:post:';
8
+ const encodeIdentifier = (identifier) => {
9
+ if (!identifier || identifier.length > 255 || /[\u0000-\u001F\u007F]/.test(identifier)) {
10
+ return;
11
+ }
12
+ try {
13
+ return encodeURIComponent(identifier);
14
+ }
15
+ catch {
16
+ // Lone UTF-16 surrogates cannot name a URI resource.
17
+ return;
18
+ }
19
+ };
20
+ /** Decode a URL path segment once. Builders always take the resulting raw value. */
21
+ export const decodeCacheTagPathSegment = (segment) => {
22
+ try {
23
+ const raw = decodeURIComponent(segment);
24
+ return encodeIdentifier(raw) === undefined ? undefined : raw;
25
+ }
26
+ catch {
27
+ return;
28
+ }
29
+ };
30
+ export const deploymentTag = (deployment) => `landing:deployment:${deployment}`;
31
+ export const marketplaceListTag = (kind) => `landing:marketplace:${kind}:list`;
32
+ /** The identifier is raw: a literal `%2F` is distinct from a slash. */
33
+ export const marketplaceItemTag = (kind, identifier) => {
34
+ const encoded = encodeIdentifier(identifier);
35
+ return encoded === undefined ? undefined : `landing:marketplace:${kind}:item:${encoded}`;
36
+ };
37
+ /** The slug is raw, including any literal percent characters. */
38
+ export const blogPostTag = (slug) => {
39
+ const encoded = encodeIdentifier(slug);
40
+ return encoded === undefined ? undefined : `${BLOG_POST_PREFIX}${encoded}`;
41
+ };
42
+ /** Format recognition only; entrypoint and purge permissions belong to the caller. */
43
+ export const isCanonicalBlogPostTag = (tag) => {
44
+ if (!tag.startsWith(BLOG_POST_PREFIX))
45
+ return false;
46
+ const slug = decodeCacheTagPathSegment(tag.slice(BLOG_POST_PREFIX.length));
47
+ return slug !== undefined && blogPostTag(slug) === tag;
48
+ };
49
+ /** Recognize only canonical detail tags for a supported marketplace kind. */
50
+ export const isCanonicalMarketplaceItemTag = (tag) => {
51
+ const match = /^landing:marketplace:(mcp|skill):item:(.+)$/.exec(tag);
52
+ if (!match)
53
+ return false;
54
+ const identifier = decodeCacheTagPathSegment(match[2]);
55
+ return identifier !== undefined && marketplaceItemTag(match[1], identifier) === tag;
56
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/edge-contract",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "One implementation of the edge logic shared by the LobeHub gateway, landing, cloud and lobe-chat, plus golden fixtures every copy is tested against.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,6 +10,10 @@
10
10
  "type": "module",
11
11
  "sideEffects": false,
12
12
  "exports": {
13
+ "./cache-tags": {
14
+ "types": "./dist/cache-tags.d.ts",
15
+ "default": "./dist/cache-tags.js"
16
+ },
13
17
  "./landing-locales": {
14
18
  "types": "./dist/landing-locales.d.ts",
15
19
  "default": "./dist/landing-locales.js"