@fayz-ai/plugin-blog 0.12.25 → 0.12.26

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/index.d.ts CHANGED
@@ -16,16 +16,19 @@ export declare function createBlogPlugin(options?: BlogPluginOptions): BlogWebsi
16
16
  export type { BlogPluginOptions, ResolvedBlogConfig } from './config';
17
17
  export type { BlogPost, BlogListQuery, BlogAuthor, BlogCategory } from './types';
18
18
  export type { BlogDataProvider } from './data/types';
19
- export { createMockBlogProvider, createSupabaseBlogProvider } from './data';
19
+ export { createMockBlogProvider } from './data/mock';
20
+ export { createSupabaseBlogProvider } from './data/supabase';
20
21
  export type { BlogSeed, BlogSeedPost } from './data/mock';
21
22
  export { BlogProvider, useBlogContext } from './context';
22
23
  export type { BlogContextValue } from './context';
23
- export { useBlogPosts, usePost } from './hooks';
24
+ export { useBlogPosts } from './hooks/useBlogPosts';
25
+ export { usePost } from './hooks/usePost';
24
26
  export { registerBlogLoaders, BLOG_POSTS_LOADER, BLOG_POST_LOADER, } from './data/loaders';
25
27
  export type { BlogPostsParams, BlogPostParams } from './data/loaders';
26
28
  export { setBlogProvider, getBlogProvider, hasBlogProvider, resetBlogProvider } from './data/runtime';
27
29
  export { buildBlogDomainEvents, BLOG_POST_EVENT_FIELDS } from './lib/blog-events';
28
30
  export { postSeo, blogListSeo, articleJsonLd, blogJsonLd, postUrl, blogUrl } from './lib/seo';
29
31
  export type { BlogSeoDescriptor, BlogSeoOptions } from './lib/seo';
30
- export type { UseBlogPostsResult, UsePostResult } from './hooks';
32
+ export type { UseBlogPostsResult } from './hooks/useBlogPosts';
33
+ export type { UsePostResult } from './hooks/usePost';
31
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAClG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAGhE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAA;AA2D/D,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,QAAQ,EAAE,cAAc,CAAA;IACxB,sFAAsF;IACtF,QAAQ,EAAE,EAAE,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACrC,2EAA2E;IAC3E,YAAY,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAA;CAC3C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,CAyJ/E;AAGD,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACrE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAChF,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,QAAQ,CAAA;AAC3E,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAI/C,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAIrG,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAGjF,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC7F,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAClE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAClG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAGhE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAA;AA2D/D,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,QAAQ,EAAE,cAAc,CAAA;IACxB,sFAAsF;IACtF,QAAQ,EAAE,EAAE,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACrC,2EAA2E;IAC3E,YAAY,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAA;CAC3C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,CAyJ/E;AAGD,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACrE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAChF,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAA;AAC5D,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAIzC,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAIrG,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAGjF,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC7F,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts","../src/data/mock.ts","../src/data/supabase.ts","../src/migrations/index.ts","../src/data/runtime.ts","../src/data/loaders.ts","../src/lib/blog-events.ts","../src/index.ts"],"names":[],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAy5B/B,IAAM,wCAAA,GAA2C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","file":"index.js","sourcesContent":["import type { PluginScope, VerticalId } from '@fayz-ai/core'\nimport type { BlogDataProvider } from './data/types'\nimport type { BlogSeed } from './data/mock'\nimport type { BlogAuthor } from './types'\n\nexport interface BlogPluginOptions {\n /** Base path the plugin mounts its public routes under. Default '/blog'. */\n basePath?: string\n /** Seed posts for the mock provider (used until a real backend is wired). */\n seed?: BlogSeed\n /** Default byline author, applied to any post that doesn't specify its own. */\n defaultAuthor?: BlogAuthor\n /** Publisher/brand name — used for SEO (og:site_name, JSON-LD publisher). */\n siteName?: string\n /** Inject a custom provider (e.g. a CMS). Overrides the safe mock/Supabase resolver. */\n dataProvider?: BlogDataProvider\n /** Tenant whose PUBLISHED posts the public (anon) site reads from Supabase.\n * Required for the Supabase provider on an anon website (mirrors how the\n * public booking plugin takes an explicit tenantId). Without it the safe\n * resolver falls back to the mock/seed provider. */\n tenantId?: string\n /** Section eyebrow / labels for the default list screen. */\n labels?: {\n /** Title shown at the top of the /blog list screen. */\n listTitle?: string\n /** Subtitle under the list title. */\n listSubtitle?: string\n /** \"Back to blog\" link label on the detail screen. */\n backToList?: string\n }\n scope?: PluginScope\n verticalId?: VerticalId\n}\n\nexport interface ResolvedBlogConfig {\n basePath: string\n siteName: string\n labels: {\n listTitle: string\n listSubtitle: string\n backToList: string\n }\n}\n\nexport function resolveConfig(options?: BlogPluginOptions): ResolvedBlogConfig {\n return {\n basePath: options?.basePath ?? '/blog',\n siteName: options?.siteName ?? '',\n labels: {\n listTitle: options?.labels?.listTitle ?? 'Blog',\n listSubtitle: options?.labels?.listSubtitle ?? '',\n backToList: options?.labels?.backToList ?? '← Voltar ao blog',\n },\n }\n}\n","import type { BlogDataProvider } from './types'\nimport type { BlogPost, BlogListQuery, BlogAuthor } from '../types'\n\n/** Slugify a title when a seed post omits an explicit slug. */\nfunction slugify(input: string): string {\n return input\n .normalize('NFD')\n // Drop combining accent marks (and any other non-ASCII) so \"saúde\" -> \"saude\".\n .replace(/[^\\x00-\\x7f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '')\n .slice(0, 80)\n}\n\n/** Seed post — same shape as BlogPost but slug/body/author may be derived. */\nexport type BlogSeedPost = Omit<BlogPost, 'slug' | 'body' | 'author'> & {\n slug?: string\n body?: string\n author?: BlogAuthor\n}\n\nexport interface BlogSeed {\n posts: BlogSeedPost[]\n}\n\nexport interface MockBlogProviderOptions {\n seed?: BlogSeed\n /** Fallback byline used for posts without their own author. */\n defaultAuthor?: BlogAuthor\n}\n\nconst FALLBACK_AUTHOR: BlogAuthor = { name: 'Equipe' }\n\nfunction normalize(post: BlogSeedPost, defaultAuthor: BlogAuthor): BlogPost {\n return {\n ...post,\n slug: post.slug ?? slugify(post.title),\n body: post.body ?? post.excerpt,\n author: post.author ?? defaultAuthor,\n }\n}\n\n/** A built-in fallback post so an unseeded plugin still renders something. */\nconst FALLBACK: BlogSeedPost[] = [\n {\n title: 'Bem-vindo ao blog',\n excerpt: 'Este é um artigo de exemplo servido pelo plugin de blog do Fayz.',\n tag: 'Novidades',\n readTime: '2 min',\n date: 'Hoje',\n image: 'https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=800&q=80',\n },\n]\n\nexport function createMockBlogProvider(options?: MockBlogProviderOptions): BlogDataProvider {\n const defaultAuthor = options?.defaultAuthor ?? FALLBACK_AUTHOR\n const posts: BlogPost[] = (options?.seed?.posts ?? FALLBACK).map((p) => normalize(p, defaultAuthor))\n\n return {\n async listPosts(query?: BlogListQuery): Promise<BlogPost[]> {\n let result = posts\n if (query?.tag) result = result.filter((p) => p.tag === query.tag)\n if (query?.limit != null) result = result.slice(0, query.limit)\n return result\n },\n async getPost(slug: string): Promise<BlogPost | null> {\n return posts.find((p) => p.slug === slug) ?? null\n },\n }\n}\n","import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'\nimport type { BlogDataProvider } from './types'\nimport type { BlogPost, BlogListQuery, BlogAuthor } from '../types'\n\n// ---------------------------------------------------------------------------\n// Supabase-backed blog provider (public/website read side).\n//\n// Reads PUBLISHED posts from the anon-safe view public.v_public_blog_posts\n// (see src/migrations/001_blog.sql §4), scoped to a tenant. The backoffice\n// writes to plg_blog_posts directly via the generic CRUD provider; here we only\n// read the published, column-whitelisted view — safe under the publishable key.\n//\n// The tenant is taken from the configured tenantId (anon site) or, failing that,\n// the active session tenant. Without a client or a tenant we return nothing, and\n// createSafeDataProvider falls back to the mock/seed provider.\n// ---------------------------------------------------------------------------\n\nconst PUBLIC_VIEW = 'v_public_blog_posts'\n\nexport interface SupabaseBlogProviderOptions {\n /** Tenant whose published posts to read (anon website). Defaults to the active session tenant. */\n tenantId?: string\n}\n\nfunction getClient() {\n const supabase = getSupabaseClientOptional() as {\n from: (t: string) => any\n } | null\n if (!supabase) throw new Error('[plugin-blog] Supabase client not initialized')\n return supabase\n}\n\n/** Human month-year label (pt-BR) derived from an ISO date, e.g. \"Janeiro 2025\". */\nfunction humanDate(iso?: string | null): string {\n if (!iso) return ''\n const d = new Date(iso)\n if (Number.isNaN(d.getTime())) return ''\n const label = d.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\nfunction toPost(row: Record<string, any>): BlogPost {\n const author: BlogAuthor = {\n name: row.author_name ?? 'Equipe',\n avatarUrl: row.author_avatar_url ?? undefined,\n role: row.author_role ?? undefined,\n bio: row.author_bio ?? undefined,\n }\n return {\n slug: row.slug,\n title: row.title,\n excerpt: row.excerpt ?? '',\n body: row.body ?? '',\n tag: row.tag ?? '',\n readTime: row.read_time ?? '',\n date: humanDate(row.published_at),\n publishedAt: row.published_at ?? undefined,\n image: row.cover_image ?? '',\n author,\n }\n}\n\nexport function createSupabaseBlogProvider(options?: SupabaseBlogProviderOptions): BlogDataProvider {\n const resolveTenantId = (): string | undefined => options?.tenantId ?? getActiveTenantId() ?? undefined\n\n return {\n async listPosts(query?: BlogListQuery): Promise<BlogPost[]> {\n const tenantId = resolveTenantId()\n if (!tenantId) return []\n let q = getClient()\n .from(PUBLIC_VIEW)\n .select('*')\n .eq('tenant_id', tenantId)\n .order('published_at', { ascending: false, nullsFirst: false })\n if (query?.tag) q = q.eq('tag', query.tag)\n if (query?.limit != null) q = q.limit(query.limit)\n const { data, error } = await q\n if (error) throw error\n return (data ?? []).map(toPost)\n },\n\n async getPost(slug: string): Promise<BlogPost | null> {\n const tenantId = resolveTenantId()\n if (!tenantId) return null\n const { data, error } = await getClient()\n .from(PUBLIC_VIEW)\n .select('*')\n .eq('tenant_id', tenantId)\n .eq('slug', slug)\n .maybeSingle()\n if (error) throw error\n return data ? toPost(data) : null\n },\n }\n}\n","// AUTO-GENERATED from 000_baseline.sql, 001_the_author_is_a_membership.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_000_BASELINE = `-- ============================================================================\n-- plugins/plugin-blog/src/migrations/000_baseline.sql — what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 2 migration files. They are kept, unapplied, in\n-- migrations.archive/ — a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- ── default privileges: reset ───────────────────────────────────────────\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\n\n-- ── table ───────────────────────────────────────────────────────────────\n\nCREATE TABLE public.plg_blog_categories (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n slug text NOT NULL,\n description text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE public.plg_blog_posts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n category_id uuid,\n slug text NOT NULL,\n title text NOT NULL,\n excerpt text DEFAULT ''::text NOT NULL,\n body text DEFAULT ''::text NOT NULL,\n cover_image text,\n author_name text,\n author_role text,\n author_avatar_url text,\n author_bio text,\n status text DEFAULT 'draft'::text NOT NULL,\n read_time text,\n published_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n meta_title text,\n meta_description text,\n canonical_url text,\n og_image text,\n noindex boolean DEFAULT false NOT NULL,\n external_ref text,\n source text DEFAULT 'admin'::text NOT NULL,\n language text,\n tags text[],\n author_member_id uuid,\n CONSTRAINT plg_blog_posts_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'published'::text])))\n);\n\n\n-- ── function ────────────────────────────────────────────────────────────\n\nCREATE FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_denial jsonb;\n v_used int;\n v_title text;\n v_base text;\n v_slug text;\n v_n int := 1;\n v_id uuid;\nBEGIN\n v_title := nullif(btrim(p_payload->>'title'), '');\n IF v_title IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'error', 'title is required to draft a post');\n END IF;\n\n SELECT count(*) INTO v_used FROM public.plg_blog_posts p WHERE p.tenant_id = p_tenant_id;\n v_denial := public.agent_guard(p_tenant_id, p_actor_user_id, 'blog', 'create', 'blog_posts', v_used, 1);\n IF v_denial IS NOT NULL THEN\n RETURN jsonb_build_object('ok', false, 'denial', v_denial);\n END IF;\n\n v_base := coalesce(nullif(public.plg_blog_slugify(v_title), ''), 'post-' || left(md5(v_title), 8));\n v_slug := left(v_base, 120);\n WHILE EXISTS (SELECT 1 FROM public.plg_blog_posts p\n WHERE p.tenant_id = p_tenant_id AND p.slug = v_slug) LOOP\n v_n := v_n + 1;\n IF v_n > 50 THEN\n RETURN jsonb_build_object('ok', false, 'error', format('cannot derive a free slug from %L', v_base));\n END IF;\n v_slug := left(v_base, 112) || '-' || v_n;\n END LOOP;\n\n INSERT INTO public.plg_blog_posts (\n tenant_id, slug, title, excerpt, body, status, source, language, tags, updated_at)\n VALUES (\n p_tenant_id, v_slug, v_title,\n left(btrim(regexp_replace(coalesce(p_payload->>'excerpt', ''), '<[^>]*>', '', 'g')), 500),\n -- Same sanitiser as the external door: the model's output is not more\n -- trustworthy than a partner's, it is just closer to home.\n public.plg_blog_sanitize_html(p_payload->>'body_html'),\n 'draft', 'agent',\n nullif(btrim(p_payload->>'language'), ''),\n CASE WHEN jsonb_typeof(p_payload->'tags') = 'array'\n THEN ARRAY(SELECT btrim(x) FROM jsonb_array_elements_text(p_payload->'tags') AS x WHERE btrim(x) <> '')\n END,\n now())\n RETURNING id INTO v_id;\n\n INSERT INTO public.audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)\n VALUES (p_tenant_id, p_actor_user_id, 'agent.draftBlogPost', 'blog_post', v_id::text,\n jsonb_build_object('title', v_title, 'slug', v_slug));\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_id,\n 'record', jsonb_build_object(\n 'ref', jsonb_build_object('id', v_id, 'resource', 'plg_blog_posts', 'entityKey', 'blog:posts'),\n 'name', v_title,\n 'slug', v_slug,\n 'status', 'draft'));\nEND;\n$$;\n\nCREATE FUNCTION public.blog_upsert_external_post(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_claimed uuid;\n v_ref text;\n v_title text;\n v_base text;\n v_slug text;\n v_status text;\n v_source text;\n v_lang text;\n v_body text;\n v_excerpt text;\n v_tags text[];\n v_author uuid;\n v_pub timestamptz;\n v_cover text;\n v_existing_id uuid;\n v_existing_pub timestamptz;\n v_existing_slug text;\n v_n int := 1;\n v_id uuid;\n v_created boolean := false;\nBEGIN\n -- ── tenant: from the credential, never from the payload ──────────────────\n -- A tenant id the caller supplies is how one tenant writes into another's\n -- blog, and the caller is a machine outside our trust boundary. The only\n -- statement about identity we can believe is the one the credential makes.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF v_tenants IS NULL OR array_length(v_tenants, 1) <> 1 THEN\n RAISE EXCEPTION\n 'blog_upsert_external_post: the caller must present a credential scoped to exactly one tenant'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n v_tenant := v_tenants[1];\n\n -- Refusing beats ignoring: a publisher that believes it is writing into\n -- another tenant must be told so, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL THEN\n BEGIN\n v_claimed := (p_payload->>'tenant_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_claimed := NULL;\n END;\n IF v_claimed IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'blog_upsert_external_post: payload names another tenant; the tenant comes from the credential'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n END IF;\n\n -- ── the article ──────────────────────────────────────────────────────────\n v_ref := nullif(btrim(p_payload->>'external_ref'), '');\n v_title := nullif(btrim(p_payload->>'title'), '');\n IF v_title IS NULL THEN\n RAISE EXCEPTION 'blog_upsert_external_post: title is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n v_status := lower(coalesce(nullif(btrim(p_payload->>'status'), ''), 'draft'));\n IF v_status NOT IN ('draft', 'published') THEN\n RAISE EXCEPTION 'blog_upsert_external_post: unknown status %; expected draft or published', v_status\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n -- Never taken as 'admin' by default: a post that arrived through this door is\n -- external by definition, and mislabelling it is what makes a connector echo\n -- it back to the system that sent it.\n v_source := lower(coalesce(nullif(btrim(p_payload->>'source'), ''), 'external'));\n v_lang := nullif(btrim(p_payload->>'language'), '');\n v_body := public.plg_blog_sanitize_html(p_payload->>'body_html');\n v_excerpt := left(btrim(regexp_replace(coalesce(p_payload->>'excerpt', ''), '<[^>]*>', '', 'g')), 500);\n\n IF jsonb_typeof(p_payload->'tags') = 'array' THEN\n SELECT array_agg(btrim(x)) INTO v_tags\n FROM jsonb_array_elements_text(p_payload->'tags') AS x\n WHERE btrim(x) <> '';\n END IF;\n\n -- An author id is a claim about a person in THIS tenant. Unverified, it would\n -- let an external publisher put a real employee's name on anything it writes.\n IF nullif(btrim(p_payload->>'author_member_id'), '') IS NOT NULL THEN\n BEGIN\n SELECT tm.id INTO v_author\n FROM public.tenant_members tm\n WHERE tm.tenant_id = v_tenant AND tm.id = (p_payload->>'author_member_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_author := NULL;\n END;\n END IF;\n\n v_cover := public.plg_blog_safe_url(\n coalesce(nullif(btrim(p_payload->>'cover_image'), ''), nullif(btrim(p_payload->>'og_image'), '')));\n\n -- ── identity + slug ──────────────────────────────────────────────────────\n IF v_ref IS NOT NULL THEN\n SELECT p.id, p.published_at, p.slug INTO v_existing_id, v_existing_pub, v_existing_slug\n FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.external_ref = v_ref;\n END IF;\n\n -- On re-delivery the existing slug WINS over one re-derived from the title.\n -- A published post's slug is its URL: silently re-slugging it because the\n -- publisher fixed a typo in the headline breaks every inbound link — on an\n -- SEO product's own articles, of all things. Changing it stays possible, but\n -- only by saying so explicitly in \\`slug\\`.\n v_base := coalesce(\n nullif(public.plg_blog_slugify(p_payload->>'slug'), ''),\n nullif(v_existing_slug, ''),\n nullif(public.plg_blog_slugify(v_title), ''),\n 'post-' || left(md5(coalesce(v_ref, gen_random_uuid()::text)), 8));\n v_base := left(v_base, 120);\n\n -- Slug is unique per tenant (001). Two different articles that slugify the\n -- same must not make the second delivery fail — a suffixed slug is a smaller\n -- surprise than a rejected publish.\n v_slug := v_base;\n WHILE EXISTS (\n SELECT 1 FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.slug = v_slug\n AND (v_existing_id IS NULL OR p.id <> v_existing_id)\n ) LOOP\n v_n := v_n + 1;\n IF v_n > 50 THEN\n RAISE EXCEPTION 'blog_upsert_external_post: cannot derive a free slug from %', v_base\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n v_slug := left(v_base, 112) || '-' || v_n;\n END LOOP;\n\n BEGIN\n v_pub := nullif(btrim(p_payload->>'published_at'), '')::timestamptz;\n EXCEPTION WHEN OTHERS THEN\n v_pub := NULL;\n END;\n -- A post that goes back to draft keeps the date it first went out; the public\n -- view filters on status, so nothing leaks, and the history stays true.\n v_pub := coalesce(v_pub, v_existing_pub, CASE WHEN v_status = 'published' THEN now() END);\n\n -- ── write ────────────────────────────────────────────────────────────────\n IF v_existing_id IS NOT NULL THEN\n UPDATE public.plg_blog_posts SET\n slug = v_slug,\n title = v_title,\n excerpt = v_excerpt,\n body = v_body,\n status = v_status,\n published_at = v_pub,\n source = v_source,\n language = v_lang,\n tags = v_tags,\n author_member_id = coalesce(v_author, author_member_id),\n cover_image = coalesce(v_cover, cover_image),\n meta_title = nullif(btrim(p_payload->>'meta_title'), ''),\n meta_description = nullif(btrim(p_payload->>'meta_description'), ''),\n canonical_url = public.plg_blog_safe_url(p_payload->>'canonical_url'),\n og_image = public.plg_blog_safe_url(p_payload->>'og_image'),\n noindex = coalesce((p_payload->>'noindex')::boolean, false),\n updated_at = now()\n WHERE id = v_existing_id;\n v_id := v_existing_id;\n ELSE\n -- ON CONFLICT on the partial index, not just the SELECT above: two\n -- deliveries of the same article racing each other would both find nothing\n -- and both insert. The index is what makes the second one an update.\n INSERT INTO public.plg_blog_posts (\n tenant_id, slug, title, excerpt, body, status, published_at,\n external_ref, source, language, tags, author_member_id, cover_image,\n meta_title, meta_description, canonical_url, og_image, noindex, updated_at)\n VALUES (\n v_tenant, v_slug, v_title, v_excerpt, v_body, v_status, v_pub,\n v_ref, v_source, v_lang, v_tags, v_author, v_cover,\n nullif(btrim(p_payload->>'meta_title'), ''),\n nullif(btrim(p_payload->>'meta_description'), ''),\n public.plg_blog_safe_url(p_payload->>'canonical_url'),\n public.plg_blog_safe_url(p_payload->>'og_image'),\n coalesce((p_payload->>'noindex')::boolean, false),\n now())\n ON CONFLICT (tenant_id, external_ref) WHERE external_ref IS NOT NULL\n DO UPDATE SET\n title = excluded.title,\n excerpt = excluded.excerpt,\n body = excluded.body,\n status = excluded.status,\n published_at = coalesce(excluded.published_at, plg_blog_posts.published_at),\n source = excluded.source,\n language = excluded.language,\n tags = excluded.tags,\n cover_image = coalesce(excluded.cover_image, plg_blog_posts.cover_image),\n meta_title = excluded.meta_title,\n meta_description = excluded.meta_description,\n canonical_url = excluded.canonical_url,\n og_image = excluded.og_image,\n noindex = excluded.noindex,\n updated_at = now()\n RETURNING id, (xmax = 0) INTO v_id, v_created;\n END IF;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_id,\n 'slug', v_slug,\n 'status', v_status,\n 'source', v_source,\n 'external_ref', v_ref,\n 'created', coalesce(v_created, false));\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_emit_post_event() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_row jsonb;\n v_tenant uuid;\n v_id text;\n v_payload jsonb;\nBEGIN\n -- A save that changed nothing is not a fact; without this any bulk re-save\n -- buries the real updates.\n IF TG_OP = 'UPDATE' AND OLD IS NOT DISTINCT FROM NEW THEN\n RETURN NULL;\n END IF;\n\n IF TG_OP = 'DELETE' THEN\n v_row := to_jsonb(OLD);\n ELSE\n v_row := to_jsonb(NEW);\n END IF;\n v_tenant := nullif(v_row->>'tenant_id', '')::uuid;\n v_id := v_row->>'id';\n -- The contract every consumer reads. Not the row: a whole-row payload puts\n -- the article body inside every webhook by default, and whoever needs it\n -- fetches by id.\n v_payload := jsonb_build_object(\n 'post_id', v_id,\n 'slug', v_row->>'slug',\n 'title', v_row->>'title',\n 'status', v_row->>'status',\n 'source', coalesce(v_row->>'source', 'admin'),\n 'external_ref', v_row->>'external_ref',\n 'language', v_row->>'language');\n\n BEGIN\n IF TG_OP = 'INSERT' THEN\n PERFORM public.plg_emit_event('blog.post.created', v_payload, 'blog_post', v_id, v_tenant, NULL);\n IF NEW.status = 'published' THEN\n PERFORM public.plg_emit_event('blog.post.published', v_payload, 'blog_post', v_id, v_tenant, NULL);\n END IF;\n ELSIF TG_OP = 'UPDATE' THEN\n PERFORM public.plg_emit_event('blog.post.updated', v_payload, 'blog_post', v_id, v_tenant, NULL);\n IF NEW.status = 'published' AND OLD.status IS DISTINCT FROM 'published' THEN\n PERFORM public.plg_emit_event('blog.post.published', v_payload, 'blog_post', v_id, v_tenant, NULL);\n END IF;\n ELSE\n PERFORM public.plg_emit_event('blog.post.deleted', v_payload, 'blog_post', v_id, v_tenant, NULL);\n END IF;\n EXCEPTION WHEN OTHERS THEN\n -- Observability must never be able to reject the write that caused it.\n RAISE WARNING '[plg_blog_emit_post_event] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;\n END;\n\n RETURN NULL;\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) RETURNS text\n LANGUAGE sql IMMUTABLE\n AS $$\n SELECT coalesce(\n substring(p_tag from '(?i)\\\\s' || p_name || '\\\\s*=\\\\s*\"([^\"]*)\"'),\n substring(p_tag from '(?i)\\\\s' || p_name || '\\\\s*=\\\\s*''([^'']*)'''),\n substring(p_tag from '(?i)\\\\s' || p_name || '\\\\s*=\\\\s*([^\\\\s\">]+)')\n );\n$$;\n\nCREATE FUNCTION public.plg_blog_html_escape(p_text text) RETURNS text\n LANGUAGE sql IMMUTABLE\n AS $$\n SELECT replace(replace(replace(coalesce(p_text, ''), '<', '&lt;'), '>', '&gt;'), '\"', '&quot;');\n$$;\n\nCREATE FUNCTION public.plg_blog_safe_url(p_url text) RETURNS text\n LANGUAGE plpgsql IMMUTABLE\n AS $$\nDECLARE\n v text;\nBEGIN\n IF p_url IS NULL THEN RETURN NULL; END IF;\n -- Whitespace and control characters go BEFORE the scheme test: \\`java\\\\tscript:\\`\n -- is a working URL in every browser and would pass a naive prefix check.\n v := regexp_replace(btrim(p_url), '[[:space:][:cntrl:]]', '', 'g');\n IF v = '' THEN RETURN NULL; END IF;\n -- Allowlist of schemes. \\`data:\\` is absent on purpose: a data URL is a way to\n -- ship a whole document — including an SVG that scripts — inside an href.\n IF v ~* '^(https?://|mailto:|/|#)' THEN\n RETURN public.plg_blog_html_escape(v);\n END IF;\n RETURN NULL;\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_sanitize_html(p_html text) RETURNS text\n LANGUAGE plpgsql IMMUTABLE\n SET search_path TO 'public'\n AS $$\nDECLARE\n -- The allowlist is what an ARTICLE is made of. Layout tags (div, span,\n -- section, main) are deliberately out: they carry nothing a blog post needs\n -- and every one of them is a place to hang an attribute. Anything not listed\n -- loses its tag and keeps its text, so a stripped wrapper degrades to prose\n -- rather than to a hole in the article.\n c_allowed constant text[] := ARRAY[\n 'p','br','hr','h1','h2','h3','h4','h5','h6',\n 'strong','b','em','i','u','s','sup','sub','code','pre','blockquote','q','cite',\n 'ul','ol','li','dl','dt','dd',\n 'a','img','figure','figcaption',\n 'table','thead','tbody','tfoot','tr','th','td','caption'\n ];\n c_void constant text[] := ARRAY['br','hr','img'];\n -- Elements whose CONTENT is code rather than text.\n c_active constant text :=\n 'script|style|iframe|object|embed|applet|form|noscript|svg|math|template|frameset|frame';\n v text;\n v_rest text;\n v_out text := '';\n v_lt int;\n v_gt int;\n v_tag text;\n v_name text;\n v_val text;\n v_attrs text;\nBEGIN\n IF p_html IS NULL OR btrim(p_html) = '' THEN RETURN ''; END IF;\n v := p_html;\n\n -- Comments first: a conditional comment is executable markup in older\n -- engines, and \\`<!-- <script>… -->\\` would otherwise reach the pass below\n -- already commented out and survive it.\n v := regexp_replace(v, '<!--.*?-->', '', 'g');\n v := regexp_replace(v, '<!\\\\[CDATA\\\\[.*?\\\\]\\\\]>', '', 'g');\n v := regexp_replace(v, '<\\\\?.*?\\\\?>', '', 'g');\n\n -- Active elements go whole. Dropping only the tag would leave the script body\n -- standing as visible page text — the article would read as vandalised even\n -- though nothing executed.\n -- Two Postgres regex traps, both of which fail OPEN and were caught by\n -- running this against a container rather than by reading it:\n -- \\`\\\\y\\`, not \\`\\\\b\\` — in a Postgres RE \\`\\\\b\\` is the BACKSPACE character, so\n -- \\`<script\\\\b\\` matches nothing and the script body survives as page text.\n -- Every quantifier non-greedy — a Postgres RE takes its greediness from its\n -- FIRST quantifier, so one greedy \\`\\\\s*\\` at the front makes the trailing\n -- \\`.*?\\` greedy too, and the first <script> swallows the article down to the\n -- last closing tag in the document.\n v := regexp_replace(v, '<\\\\s*?(' || c_active || ')\\\\y[^>]*?>.*?<\\\\s*?/\\\\s*?\\\\1\\\\s*?>', '', 'gi');\n -- …and the unbalanced halves of the same set, plus the document-level tags\n -- that have no business inside an article body.\n v := regexp_replace(\n v,\n '<\\\\s*/?\\\\s*(' || c_active || '|link|meta|base|title|head|body|html)\\\\y[^>]*>',\n '', 'gi');\n\n -- Walk the remaining tags. A rewrite rather than a filter: every tag that\n -- comes out the other side was BUILT here from a name and an allowlisted\n -- attribute, so nothing an attacker wrote is copied through by accident.\n v_rest := v;\n LOOP\n v_lt := position('<' in v_rest);\n IF v_lt = 0 THEN\n v_out := v_out || v_rest;\n EXIT;\n END IF;\n v_out := v_out || substr(v_rest, 1, v_lt - 1);\n v_rest := substr(v_rest, v_lt);\n\n v_gt := position('>' in v_rest);\n IF v_gt = 0 THEN\n -- An unterminated '<' is text. It is escaped so it cannot open a tag when\n -- something is concatenated after this body.\n v_out := v_out || replace(v_rest, '<', '&lt;');\n EXIT;\n END IF;\n v_tag := substr(v_rest, 1, v_gt);\n v_rest := substr(v_rest, v_gt + 1);\n\n v_name := lower(coalesce(substring(v_tag from '^<\\\\s*/?\\\\s*([a-zA-Z][a-zA-Z0-9]*)'), ''));\n IF v_name = '' OR NOT (v_name = ANY (c_allowed)) THEN\n CONTINUE; -- tag dropped, its text kept\n END IF;\n\n IF v_tag ~ '^<\\\\s*/' THEN\n IF NOT (v_name = ANY (c_void)) THEN v_out := v_out || '</' || v_name || '>'; END IF;\n CONTINUE;\n END IF;\n\n -- Attributes are an allowlist too, and a per-tag one: \\`href\\` on <a>,\n -- \\`src\\`/\\`alt\\` on <img>. Everything else — style, on*, srcset, class,\n -- data-* — is dropped rather than inspected, because inspecting means\n -- maintaining a list of what is dangerous, and that list is never finished.\n v_attrs := '';\n IF v_name = 'a' THEN\n v_val := public.plg_blog_safe_url(public.plg_blog_html_attr(v_tag, 'href'));\n IF v_val IS NOT NULL THEN\n -- An external publisher's outbound links are not the tenant's\n -- endorsement, and an unmarked link farm is precisely the SEO damage\n -- this seam could otherwise cause on the tenant's own domain.\n v_attrs := ' href=\"' || v_val || '\" rel=\"nofollow noopener\"';\n END IF;\n ELSIF v_name = 'img' THEN\n v_val := public.plg_blog_safe_url(public.plg_blog_html_attr(v_tag, 'src'));\n IF v_val IS NULL THEN CONTINUE; END IF; -- an <img> with no safe src is nothing\n v_attrs := ' src=\"' || v_val || '\" alt=\"'\n || public.plg_blog_html_escape(public.plg_blog_html_attr(v_tag, 'alt')) || '\"';\n END IF;\n\n v_out := v_out || '<' || v_name || v_attrs\n || CASE WHEN v_name = ANY (c_void) THEN ' />' ELSE '>' END;\n END LOOP;\n\n RETURN v_out;\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_slugify(p_text text) RETURNS text\n LANGUAGE sql IMMUTABLE\n AS $$\n SELECT btrim(\n regexp_replace(\n translate(lower(coalesce(p_text, '')),\n 'áàâãäåéèêëíìîïóòôõöøúùûüçñýÿ',\n 'aaaaaaeeeeiiiioooooouuuucnyy'),\n '[^a-z0-9]+', '-', 'g'),\n '-');\n$$;\n\n\n-- ── constraint ──────────────────────────────────────────────────────────\n\nALTER TABLE ONLY public.plg_blog_categories\n ADD CONSTRAINT plg_blog_categories_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_pkey PRIMARY KEY (id);\n\n\n-- ── view ────────────────────────────────────────────────────────────────\n\nCREATE VIEW public.v_public_blog_posts AS\n SELECT p.id,\n p.tenant_id,\n p.slug,\n p.title,\n p.excerpt,\n p.body,\n COALESCE(c.name, ''::text) AS tag,\n p.cover_image,\n p.author_name,\n p.author_role,\n p.author_avatar_url,\n p.author_bio,\n p.read_time,\n p.published_at,\n p.meta_title,\n p.meta_description,\n p.canonical_url,\n p.og_image,\n p.noindex,\n p.language,\n p.tags\n FROM (public.plg_blog_posts p\n LEFT JOIN public.plg_blog_categories c ON ((c.id = p.category_id)))\n WHERE (p.status = 'published'::text);\n\n\n-- ── index ───────────────────────────────────────────────────────────────\n\nCREATE INDEX idx_plg_blog_categories_tenant ON public.plg_blog_categories USING btree (tenant_id);\n\nCREATE INDEX idx_plg_blog_posts_category ON public.plg_blog_posts USING btree (category_id);\n\nCREATE INDEX idx_plg_blog_posts_source ON public.plg_blog_posts USING btree (tenant_id, source, updated_at DESC);\n\nCREATE INDEX idx_plg_blog_posts_status ON public.plg_blog_posts USING btree (tenant_id, status, published_at DESC);\n\nCREATE INDEX idx_plg_blog_posts_tenant ON public.plg_blog_posts USING btree (tenant_id);\n\nCREATE UNIQUE INDEX uq_plg_blog_categories_tenant_slug ON public.plg_blog_categories USING btree (tenant_id, slug);\n\nCREATE UNIQUE INDEX uq_plg_blog_posts_tenant_external_ref ON public.plg_blog_posts USING btree (tenant_id, external_ref) WHERE (external_ref IS NOT NULL);\n\nCREATE UNIQUE INDEX uq_plg_blog_posts_tenant_slug ON public.plg_blog_posts USING btree (tenant_id, slug);\n\n\n-- ── fk constraint ───────────────────────────────────────────────────────\n\nALTER TABLE ONLY public.plg_blog_categories\n ADD CONSTRAINT plg_blog_categories_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_author_member_id_fkey FOREIGN KEY (author_member_id) REFERENCES public.tenant_members(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.plg_blog_categories(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\n\n-- ── trigger ─────────────────────────────────────────────────────────────\n\nCREATE TRIGGER plg_blog_posts_emit_facts AFTER INSERT OR DELETE OR UPDATE ON public.plg_blog_posts FOR EACH ROW EXECUTE FUNCTION public.plg_blog_emit_post_event();\n\n\n-- ── row security ────────────────────────────────────────────────────────\n\nALTER TABLE public.plg_blog_categories ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_blog_posts ENABLE ROW LEVEL SECURITY;\n\n\n-- ── policy ──────────────────────────────────────────────────────────────\n\nCREATE POLICY plg_blog_categories_delete ON public.plg_blog_categories FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_categories_insert ON public.plg_blog_categories FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_categories_select ON public.plg_blog_categories FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_categories_update ON public.plg_blog_categories FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_delete ON public.plg_blog_posts FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_insert ON public.plg_blog_posts FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_select ON public.plg_blog_posts FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_update ON public.plg_blog_posts FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\n\n-- ── acl ─────────────────────────────────────────────────────────────────\n\nREVOKE ALL ON FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.blog_upsert_external_post(p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.blog_upsert_external_post(p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.blog_upsert_external_post(p_payload jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_emit_post_event() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_emit_post_event() TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_emit_post_event() TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_html_escape(p_text text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_html_escape(p_text text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_html_escape(p_text text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_safe_url(p_url text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_safe_url(p_url text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_safe_url(p_url text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_sanitize_html(p_html text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_sanitize_html(p_html text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_sanitize_html(p_html text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_slugify(p_text text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_slugify(p_text text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_slugify(p_text text) TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_blog_categories TO anon;\nGRANT ALL ON TABLE public.plg_blog_categories TO authenticated;\nGRANT ALL ON TABLE public.plg_blog_categories TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_blog_posts TO anon;\nGRANT ALL ON TABLE public.plg_blog_posts TO authenticated;\nGRANT ALL ON TABLE public.plg_blog_posts TO service_role;\n\nGRANT SELECT,MAINTAIN ON TABLE public.v_public_blog_posts TO anon;\nGRANT ALL ON TABLE public.v_public_blog_posts TO authenticated;\nGRANT ALL ON TABLE public.v_public_blog_posts TO service_role;\n\n\n-- ── default privileges: restore ─────────────────────────────────────────\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n`\n\nexport const MIGRATION_001_THE_AUTHOR_IS_A_MEMBERSHIP = `-- ============================================================================\n-- 001_the_author_is_a_membership — the FK and the lookup leave the projection\n-- (#214 / #352).\n--\n-- plg_blog_posts.author_member_id pointed at public.tenant_members(id), which\n-- is a COMPAT PROJECTION of app.memberships maintained by triggers — so a real\n-- foreign key was resting on a derived table. That is the reason the projection\n-- could not be dropped, and it is a stronger reason than \"a screen reads it\":\n-- Postgres refuses to drop a table another table references.\n--\n-- The move is safe precisely because the projection's own comment says the ids\n-- are the same (\"COMPAT PROJECTION of app.memberships (#134): same ids\"), so\n-- every existing author_member_id already names an app.memberships row.\n-- ============================================================================\n\nALTER TABLE public.plg_blog_posts DROP CONSTRAINT IF EXISTS plg_blog_posts_author_member_id_fkey;\nALTER TABLE public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_author_member_id_fkey\n FOREIGN KEY (author_member_id) REFERENCES app.memberships(id) ON DELETE SET NULL;\n\nCREATE OR REPLACE FUNCTION public.blog_upsert_external_post(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_claimed uuid;\n v_ref text;\n v_title text;\n v_base text;\n v_slug text;\n v_status text;\n v_source text;\n v_lang text;\n v_body text;\n v_excerpt text;\n v_tags text[];\n v_author uuid;\n v_pub timestamptz;\n v_cover text;\n v_existing_id uuid;\n v_existing_pub timestamptz;\n v_existing_slug text;\n v_n int := 1;\n v_id uuid;\n v_created boolean := false;\nBEGIN\n -- ── tenant: from the credential, never from the payload ──────────────────\n -- A tenant id the caller supplies is how one tenant writes into another's\n -- blog, and the caller is a machine outside our trust boundary. The only\n -- statement about identity we can believe is the one the credential makes.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF v_tenants IS NULL OR array_length(v_tenants, 1) <> 1 THEN\n RAISE EXCEPTION\n 'blog_upsert_external_post: the caller must present a credential scoped to exactly one tenant'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n v_tenant := v_tenants[1];\n\n -- Refusing beats ignoring: a publisher that believes it is writing into\n -- another tenant must be told so, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL THEN\n BEGIN\n v_claimed := (p_payload->>'tenant_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_claimed := NULL;\n END;\n IF v_claimed IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'blog_upsert_external_post: payload names another tenant; the tenant comes from the credential'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n END IF;\n\n -- ── the article ──────────────────────────────────────────────────────────\n v_ref := nullif(btrim(p_payload->>'external_ref'), '');\n v_title := nullif(btrim(p_payload->>'title'), '');\n IF v_title IS NULL THEN\n RAISE EXCEPTION 'blog_upsert_external_post: title is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n v_status := lower(coalesce(nullif(btrim(p_payload->>'status'), ''), 'draft'));\n IF v_status NOT IN ('draft', 'published') THEN\n RAISE EXCEPTION 'blog_upsert_external_post: unknown status %; expected draft or published', v_status\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n -- Never taken as 'admin' by default: a post that arrived through this door is\n -- external by definition, and mislabelling it is what makes a connector echo\n -- it back to the system that sent it.\n v_source := lower(coalesce(nullif(btrim(p_payload->>'source'), ''), 'external'));\n v_lang := nullif(btrim(p_payload->>'language'), '');\n v_body := public.plg_blog_sanitize_html(p_payload->>'body_html');\n v_excerpt := left(btrim(regexp_replace(coalesce(p_payload->>'excerpt', ''), '<[^>]*>', '', 'g')), 500);\n\n IF jsonb_typeof(p_payload->'tags') = 'array' THEN\n SELECT array_agg(btrim(x)) INTO v_tags\n FROM jsonb_array_elements_text(p_payload->'tags') AS x\n WHERE btrim(x) <> '';\n END IF;\n\n -- An author id is a claim about a person in THIS tenant. Unverified, it would\n -- let an external publisher put a real employee's name on anything it writes.\n IF nullif(btrim(p_payload->>'author_member_id'), '') IS NOT NULL THEN\n BEGIN\n SELECT tm.id INTO v_author\n FROM app.memberships tm\n WHERE tm.tenant_id = v_tenant AND tm.id = (p_payload->>'author_member_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_author := NULL;\n END;\n END IF;\n\n v_cover := public.plg_blog_safe_url(\n coalesce(nullif(btrim(p_payload->>'cover_image'), ''), nullif(btrim(p_payload->>'og_image'), '')));\n\n -- ── identity + slug ──────────────────────────────────────────────────────\n IF v_ref IS NOT NULL THEN\n SELECT p.id, p.published_at, p.slug INTO v_existing_id, v_existing_pub, v_existing_slug\n FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.external_ref = v_ref;\n END IF;\n\n -- On re-delivery the existing slug WINS over one re-derived from the title.\n -- A published post's slug is its URL: silently re-slugging it because the\n -- publisher fixed a typo in the headline breaks every inbound link — on an\n -- SEO product's own articles, of all things. Changing it stays possible, but\n -- only by saying so explicitly in \\`slug\\`.\n v_base := coalesce(\n nullif(public.plg_blog_slugify(p_payload->>'slug'), ''),\n nullif(v_existing_slug, ''),\n nullif(public.plg_blog_slugify(v_title), ''),\n 'post-' || left(md5(coalesce(v_ref, gen_random_uuid()::text)), 8));\n v_base := left(v_base, 120);\n\n -- Slug is unique per tenant (001). Two different articles that slugify the\n -- same must not make the second delivery fail — a suffixed slug is a smaller\n -- surprise than a rejected publish.\n v_slug := v_base;\n WHILE EXISTS (\n SELECT 1 FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.slug = v_slug\n AND (v_existing_id IS NULL OR p.id <> v_existing_id)\n ) LOOP\n v_n := v_n + 1;\n IF v_n > 50 THEN\n RAISE EXCEPTION 'blog_upsert_external_post: cannot derive a free slug from %', v_base\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n v_slug := left(v_base, 112) || '-' || v_n;\n END LOOP;\n\n BEGIN\n v_pub := nullif(btrim(p_payload->>'published_at'), '')::timestamptz;\n EXCEPTION WHEN OTHERS THEN\n v_pub := NULL;\n END;\n -- A post that goes back to draft keeps the date it first went out; the public\n -- view filters on status, so nothing leaks, and the history stays true.\n v_pub := coalesce(v_pub, v_existing_pub, CASE WHEN v_status = 'published' THEN now() END);\n\n -- ── write ────────────────────────────────────────────────────────────────\n IF v_existing_id IS NOT NULL THEN\n UPDATE public.plg_blog_posts SET\n slug = v_slug,\n title = v_title,\n excerpt = v_excerpt,\n body = v_body,\n status = v_status,\n published_at = v_pub,\n source = v_source,\n language = v_lang,\n tags = v_tags,\n author_member_id = coalesce(v_author, author_member_id),\n cover_image = coalesce(v_cover, cover_image),\n meta_title = nullif(btrim(p_payload->>'meta_title'), ''),\n meta_description = nullif(btrim(p_payload->>'meta_description'), ''),\n canonical_url = public.plg_blog_safe_url(p_payload->>'canonical_url'),\n og_image = public.plg_blog_safe_url(p_payload->>'og_image'),\n noindex = coalesce((p_payload->>'noindex')::boolean, false),\n updated_at = now()\n WHERE id = v_existing_id;\n v_id := v_existing_id;\n ELSE\n -- ON CONFLICT on the partial index, not just the SELECT above: two\n -- deliveries of the same article racing each other would both find nothing\n -- and both insert. The index is what makes the second one an update.\n INSERT INTO public.plg_blog_posts (\n tenant_id, slug, title, excerpt, body, status, published_at,\n external_ref, source, language, tags, author_member_id, cover_image,\n meta_title, meta_description, canonical_url, og_image, noindex, updated_at)\n VALUES (\n v_tenant, v_slug, v_title, v_excerpt, v_body, v_status, v_pub,\n v_ref, v_source, v_lang, v_tags, v_author, v_cover,\n nullif(btrim(p_payload->>'meta_title'), ''),\n nullif(btrim(p_payload->>'meta_description'), ''),\n public.plg_blog_safe_url(p_payload->>'canonical_url'),\n public.plg_blog_safe_url(p_payload->>'og_image'),\n coalesce((p_payload->>'noindex')::boolean, false),\n now())\n ON CONFLICT (tenant_id, external_ref) WHERE external_ref IS NOT NULL\n DO UPDATE SET\n title = excluded.title,\n excerpt = excluded.excerpt,\n body = excluded.body,\n status = excluded.status,\n published_at = coalesce(excluded.published_at, plg_blog_posts.published_at),\n source = excluded.source,\n language = excluded.language,\n tags = excluded.tags,\n cover_image = coalesce(excluded.cover_image, plg_blog_posts.cover_image),\n meta_title = excluded.meta_title,\n meta_description = excluded.meta_description,\n canonical_url = excluded.canonical_url,\n og_image = excluded.og_image,\n noindex = excluded.noindex,\n updated_at = now()\n RETURNING id, (xmax = 0) INTO v_id, v_created;\n END IF;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_id,\n 'slug', v_slug,\n 'status', v_status,\n 'source', v_source,\n 'external_ref', v_ref,\n 'created', coalesce(v_created, false));\nEND;\n$$;`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n { id: \"001_the_author_is_a_membership\", sql: MIGRATION_001_THE_AUTHOR_IS_A_MEMBERSHIP },\n]\n","import type { BlogDataProvider } from './types'\n\n// ---------------------------------------------------------------------------\n// The active blog provider, reachable outside React.\n//\n// The hooks read the provider from context, which is right for a component but\n// useless to a loader: a loader is a plain function the page resolver calls\n// before (and without) rendering, so it cannot be inside a provider. Registering\n// the provider here is what lets `blog.posts` and `blog.post` be resolved by a\n// server ahead of render — the same shape `@fayz-ai/shop` uses for its own port.\n//\n// One provider per process, set by `createBlogPlugin`. That matches how a blog\n// is mounted (once, at the app root) and how a storefront is served (one store\n// per process). A multi-tenant renderer would need this request-scoped, which is\n// the same open question the shop provider has.\n// ---------------------------------------------------------------------------\n\nlet _provider: BlogDataProvider | null = null\n\nexport function setBlogProvider(provider: BlogDataProvider): void {\n _provider = provider\n}\n\n/**\n * The registered provider.\n *\n * Throws rather than returning an empty result: a loader that silently answered\n * \"no posts\" when the plugin was never mounted would render an empty blog and\n * give no reason, and the caller cannot tell that apart from a blog with no\n * posts yet.\n */\nexport function getBlogProvider(): BlogDataProvider {\n if (!_provider) {\n throw new Error(\n '[plugin-blog] no blog provider registered — call createBlogPlugin() before resolving blog loaders, and mount its Provider at the app root.',\n )\n }\n return _provider\n}\n\nexport function hasBlogProvider(): boolean {\n return _provider !== null\n}\n\n/** Test seam. */\nexport function resetBlogProvider(): void {\n _provider = null\n}\n","import { defineLoader, hasLoader } from '@fayz-ai/core'\nimport { getBlogProvider } from './runtime'\nimport type { BlogPost } from '../types'\n\n// ---------------------------------------------------------------------------\n// The blog's reads, as named loaders.\n//\n// A hook that fetches in an effect can only ever produce a loading state on a\n// server, so a blog rendered headlessly says \"Carregando…\" and a crawler indexes\n// that. Declaring the reads instead puts them in the same registry the shop's\n// loaders live in, which is what the page resolver walks before render and what\n// `renderStore` resolves between passes — so a post's body is in the HTML, and\n// two rails asking for the same list cost one provider call.\n//\n// Registered into core's registry, not the storefront's: a plugin depends on\n// core only, and that is the registry both the storefront resolver and the\n// headless renderer read.\n// ---------------------------------------------------------------------------\n\nexport const BLOG_POSTS_LOADER = 'blog.posts'\nexport const BLOG_POST_LOADER = 'blog.post'\n\nexport interface BlogPostsParams extends Record<string, unknown> {\n /** Cap the list. Omitted means every published post. */\n limit?: number\n /** Only posts carrying this tag. */\n tag?: string\n}\n\nexport interface BlogPostParams extends Record<string, unknown> {\n slug?: string\n}\n\nasync function blogPosts(params: BlogPostsParams): Promise<BlogPost[]> {\n const { limit, tag } = params\n return getBlogProvider().listPosts({ limit, tag })\n}\n\nasync function blogPost(params: BlogPostParams): Promise<BlogPost | null> {\n const slug = typeof params.slug === 'string' ? params.slug : undefined\n // A missing slug is \"no post\", not an error: a route param can legitimately\n // be absent for one render, and throwing would take the page down for it.\n if (!slug) return null\n return getBlogProvider().getPost(slug)\n}\n\n/**\n * Register the blog reads. Idempotent, so mounting a second blog (or a hot\n * reload) does not throw on a duplicate id.\n */\nexport function registerBlogLoaders(): void {\n if (!hasLoader(BLOG_POSTS_LOADER)) {\n defineLoader<BlogPostsParams, BlogPost[]>(BLOG_POSTS_LOADER, blogPosts, {\n source: 'plugin',\n title: 'Blog posts',\n description:\n 'Published posts, newest first. The read behind a blog index, a \"latest articles\" rail, or a tag archive.',\n output:\n 'Array of BlogPost (slug, title, excerpt, body, image, tag, author, publishedAt …).',\n settings: [\n {\n id: 'limit',\n type: 'number',\n label: 'How many',\n help: 'Empty means every published post.',\n min: 1,\n max: 100,\n },\n {\n id: 'tag',\n type: 'text',\n label: 'Tag',\n help: 'Only posts carrying this tag. Empty means all posts.',\n },\n ],\n })\n }\n\n if (!hasLoader(BLOG_POST_LOADER)) {\n defineLoader<BlogPostParams, BlogPost | null>(BLOG_POST_LOADER, blogPost, {\n source: 'plugin',\n title: 'Blog post',\n description: 'One post by slug. The read behind a post page.',\n output: 'A BlogPost, or null when no post has that slug.',\n settings: [\n {\n id: 'slug',\n type: 'text',\n label: 'Slug',\n help: 'Usually bound to the route param, e.g. `{ $param: \"slug\" }`.',\n },\n ],\n })\n }\n}\n","import type { EventField, PluginEventDefinition } from '@fayz-ai/core'\n\n// ---------------------------------------------------------------------------\n// The FACTS of the blog.\n//\n// The emitter is not in this file and cannot be: a post is written by the\n// backoffice, by an external publisher through blog_upsert_external_post, by\n// the agent and occasionally by hand in SQL. The one place all four end up is\n// the row, so the emitter is a trigger on plg_blog_posts (migration 002 §6) and\n// this is its contract.\n//\n// `source` is the field the whole integration turns on. It names who WROTE the\n// post — 'admin', 'ranklayer', 'agent' — and a connector reads it to decide\n// whether the fact is news or its own echo coming back. Without it, publishing\n// an article into the tenant produces an event that sends the article back to\n// the publisher, which sends it again.\n// ---------------------------------------------------------------------------\n\n/** Payload keys, in the order the trigger builds them. */\nexport const BLOG_POST_EVENT_FIELDS: EventField[] = [\n { name: 'post_id', label: 'ID do post', type: 'id', example: '8f14e45f-…' },\n { name: 'slug', label: 'Endereço', type: 'string', example: 'saude-bucal-na-terceira-idade' },\n { name: 'title', label: 'Título', type: 'string', example: 'Saúde bucal na terceira idade' },\n { name: 'status', label: 'Situação', type: 'string', example: 'published' },\n { name: 'source', label: 'Origem', type: 'string', example: 'ranklayer' },\n { name: 'external_ref', label: 'Referência externa', type: 'string', example: 'rl-article-77' },\n { name: 'language', label: 'Idioma', type: 'string', example: 'pt-BR' },\n]\n\nconst PAYLOAD_SCHEMA: Record<string, unknown> = {\n type: 'object',\n properties: {\n post_id: { type: 'string', format: 'uuid', description: 'Post id in the tenant' },\n slug: { type: 'string', description: 'URL segment under the blog base path' },\n title: { type: 'string' },\n status: { type: 'string', enum: ['draft', 'published'] },\n source: {\n type: 'string',\n description:\n \"Who wrote the post — 'admin', 'ranklayer', 'agent'. A connector compares it against its own id to avoid echoing a post back to the system it came from.\",\n },\n external_ref: { type: 'string', description: \"The writing system's own id for the article, when it has one\" },\n language: { type: 'string', description: 'BCP-47 tag, when the writer declares one' },\n },\n required: ['post_id', 'slug', 'title', 'status', 'source'],\n additionalProperties: false,\n}\n\n/**\n * `audit: true` on created/published/deleted: putting an article on the\n * company's public site and taking it down again are ACTS, and \"who published\n * this?\" is the question the trail exists to answer — sharper still when the\n * answer is a partner's connector rather than a person. `updated` stays out:\n * an SEO product rewrites the same article on a schedule, and a trail where\n * every third line is \"post updated\" is one nobody reads.\n */\nexport function buildBlogDomainEvents(): PluginEventDefinition[] {\n return [\n {\n name: 'blog.post.created',\n title: 'Post criado',\n description: 'Um artigo novo entrou no blog',\n trigger: 'Quando um post é criado — pelo painel, pelo assistente ou por um publicador externo',\n icon: 'FilePlus',\n audit: true,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n {\n name: 'blog.post.updated',\n title: 'Post alterado',\n description: 'O conteúdo ou os metadados do artigo mudaram',\n trigger: 'Quando um post é salvo com alguma alteração de verdade',\n icon: 'FilePen',\n audit: false,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n {\n name: 'blog.post.published',\n title: 'Post publicado',\n description: 'O artigo ficou visível no site',\n trigger: 'Quando o post passa a publicado — na criação ou saindo de rascunho',\n icon: 'Globe',\n audit: true,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n {\n name: 'blog.post.deleted',\n title: 'Post excluído',\n description: 'O artigo saiu do ar',\n trigger: 'Quando um post é excluído',\n icon: 'FileX',\n audit: true,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n ]\n}\n","import { createElement, lazy, Suspense, type ComponentType, type FC, type ReactNode } from 'react'\nimport type { PluginManifest } from '@fayz-ai/core'\nimport { createSafeDataProvider } from '@fayz-ai/core'\nimport { resolveConfig, type BlogPluginOptions } from './config'\nimport { createMockBlogProvider } from './data/mock'\nimport { createSupabaseBlogProvider } from './data/supabase'\nimport { BlogProvider, type BlogContextValue } from './context'\nimport { MIGRATIONS } from './migrations'\nimport { setBlogProvider } from './data/runtime'\nimport { registerBlogLoaders } from './data/loaders'\nimport { buildBlogDomainEvents } from './lib/blog-events'\n\n// ---------------------------------------------------------------------------\n// @fayz-ai/plugin-blog — website-surface blog/articles plugin.\n//\n// First real consumer of scaffolds:['website']. Ships public routes (/blog,\n// /blog/:slug) + data hooks (useBlogPosts/usePost). A bespoke host site feeds\n// its own card markup from the hooks and mounts the routes inside its chrome.\n// See createBlogPlugin's return: a { manifest, Provider } bundle the host reads\n// uniformly (manifest.routes for mounting, Provider for the app-root context).\n// ---------------------------------------------------------------------------\n\nconst MIGRATION_META: Record<string, { version: string; description: string }> = {\n '001_blog': {\n version: '1.0.0',\n description: 'Blog content model (plg_blog_categories, plg_blog_posts) + anon-read view',\n },\n '002_external_posts': {\n version: '1.1.0',\n description:\n 'Per-post SEO, external identity (external_ref/source/language/tags), blog_upsert_external_post + agent_blog_draft_post, and the blog.post.* triggers',\n },\n}\n\n// ---------------------------------------------------------------------------\n// The default route components are the ONLY part of this package that knows\n// react-router-dom, and they exist for one kind of host: a website on\n// react-router that mounts `manifest.routes` as they come. Every other\n// consumer — a storefront on the SDK's own router adapter, a TanStack Start\n// host, anything that reads through the hooks or the loaders and draws its\n// own markup — never renders them, and until now still shipped react-router\n// (and @remix-run/router underneath) to every shopper because this entry\n// imported them statically. Measured on the first SSR store: ~8% of the\n// client bundle for a router it does not use.\n//\n// So the manifest carries them LAZILY: the import is deferred to first\n// render, which puts the components and their router into a chunk of their\n// own that a host only downloads when it actually mounts one of these\n// routes. Each carries its own Suspense boundary, so a react-router host that\n// renders `route.component` directly (hempdent, seusorriso) keeps working\n// unchanged. Importing them directly is still possible, from\n// `@fayz-ai/plugin-blog/components`.\n// ---------------------------------------------------------------------------\n\nfunction lazyRoute(load: () => Promise<{ default: ComponentType<unknown> }>): ComponentType<unknown> {\n const Lazy = lazy(load)\n const Route: FC<unknown> = () => createElement(Suspense, { fallback: null }, createElement(Lazy))\n return Route\n}\n\nconst BlogListRoute = lazyRoute(() => import('./components/BlogList').then((m) => ({ default: m.BlogList as ComponentType<unknown> })))\nBlogListRoute.displayName = 'BlogListRoute'\nconst PostDetailRoute = lazyRoute(() => import('./components/PostDetail').then((m) => ({ default: m.PostDetail as ComponentType<unknown> })))\nPostDetailRoute.displayName = 'PostDetailRoute'\n\nexport interface BlogWebsitePlugin {\n /** Standard plugin manifest (routes + metadata). */\n manifest: PluginManifest\n /** App-root context provider, pre-bound to this plugin's resolved provider/config. */\n Provider: FC<{ children: ReactNode }>\n /** The resolved data provider (exposed for advanced host wiring/tests). */\n dataProvider: BlogContextValue['provider']\n}\n\nexport function createBlogPlugin(options?: BlogPluginOptions): BlogWebsitePlugin {\n const config = resolveConfig(options)\n const provider =\n options?.dataProvider ??\n createSafeDataProvider(\n () => createSupabaseBlogProvider({ tenantId: options?.tenantId }),\n () => createMockBlogProvider({ seed: options?.seed, defaultAuthor: options?.defaultAuthor }),\n )\n\n // Reachable outside React, and the reads declared, so a page resolver or a\n // headless render can answer them before anything renders. Without this a\n // server-rendered blog can only emit its loading state.\n setBlogProvider(provider)\n registerBlogLoaders()\n\n const value: BlogContextValue = { provider, config }\n const Provider: FC<{ children: ReactNode }> = ({ children }) =>\n createElement(BlogProvider, { value, children })\n Provider.displayName = 'BlogPluginProvider'\n\n const manifest: PluginManifest = {\n id: 'blog',\n name: 'Blog',\n icon: 'Newspaper',\n // Tracks package.json. It sat at '0.1.0' for nine minor releases, and a\n // partner reading the manifest concluded the plugin was a stub.\n version: '0.10.0',\n apiVersion: 1,\n scope: options?.scope ?? 'universal',\n verticalId: options?.verticalId,\n scaffolds: ['website', 'landing_page'],\n defaultEnabled: true,\n dependencies: [],\n declaredFeatures: [{ id: 'blog', label: 'Blog', group: 'Attract' }],\n // Blog posts count against the plan cap. The create UI lives in the\n // marketing \"Blog\" backoffice, which writes to this plugin's posts table.\n declaredLimits: [{ key: 'blog_posts', label: 'Blog posts', table: 'plg_blog_posts' }],\n // The facts. Emitted by a trigger on the posts table (migration 002 §6), so\n // an external publisher's write produces the same event as the backoffice's.\n events: buildBlogDomainEvents(),\n // The generic row-level ledger coverage (registry.blog_post.*), which is a\n // different vocabulary from the four above on purpose: it says a ROW\n // changed, the events say what happened to a POST. Nothing attaches this\n // yet — plg_attach_event_source is wired in a later milestone.\n emitsFrom: [{ table: 'plg_blog_posts', entityKey: 'blog_post' }],\n declaredRpcs: [\n {\n name: 'blog_upsert_external_post',\n kind: 'write' as const,\n description:\n 'Ingest door for an external publisher (RankLayer). Tenant pinned from the caller credential, idempotent on (tenant_id, external_ref), body_html sanitised against a tag allowlist in SQL.',\n audits: false,\n },\n {\n name: 'agent_blog_draft_post',\n kind: 'write' as const,\n description:\n 'Guarded blog draft: agent_guard (role → plan → blog_posts cap), body sanitised by the same allowlist, always status draft, audited.',\n audits: true,\n },\n ],\n // Without this the blog is invisible to the agent: it carries its own data\n // provider, and only queryEntities reaches the server-plane executor.\n queryEntities: [\n {\n key: 'blog:posts',\n entity: {\n name: 'Post',\n namePlural: 'Posts',\n icon: 'Newspaper',\n permission: { feature: 'blog', action: 'read' },\n fields: [\n { key: 'title', label: 'Título', type: 'text', searchable: true },\n { key: 'slug', label: 'Endereço', type: 'text', searchable: true },\n { key: 'status', label: 'Situação', type: 'text' },\n { key: 'source', label: 'Origem', type: 'text' },\n { key: 'language', label: 'Idioma', type: 'text' },\n { key: 'published_at', label: 'Publicado em', type: 'text' },\n { key: 'updated_at', label: 'Atualizado em', type: 'text' },\n ],\n data: {\n table: 'plg_blog_posts',\n tenantScoped: true,\n searchColumns: ['title', 'slug'],\n },\n },\n },\n ],\n aiTools: [\n {\n id: 'blog.list-posts',\n name: 'listBlogPosts',\n description:\n 'Lists the blog posts of this tenant — title, slug, status, source and dates. Use it to answer what has been published, what is still a draft, and what came from an external publisher.',\n icon: 'Newspaper',\n mode: 'read' as const,\n execution: { plane: 'server' as const, kind: 'entity_read' as const, entity: 'blog:posts' },\n category: 'Blog',\n parameters: {\n type: 'object' as const,\n properties: {\n status: { type: 'string' as const, enum: ['draft', 'published'] },\n search: { type: 'string' as const, description: 'Match against title or slug' },\n },\n },\n suggestions: [{ label: 'Quais posts estão em rascunho?' }, { label: 'O que foi publicado no blog este mês?' }],\n permission: { feature: 'blog', action: 'read' as const },\n },\n {\n id: 'blog.draft-post',\n name: 'draftBlogPost',\n description:\n 'Creates a blog post as a DRAFT — it never goes live from here; a person publishes it from the blog backoffice. Takes a title and the article body as simple HTML (headings, paragraphs, lists, links).',\n icon: 'FilePlus',\n mode: 'persist' as const,\n limitKey: 'blog_posts',\n execution: { plane: 'server' as const, kind: 'rpc' as const, rpc: 'agent_blog_draft_post' },\n category: 'Blog',\n parameters: {\n type: 'object' as const,\n properties: {\n title: { type: 'string' as const, description: 'Post title' },\n body_html: { type: 'string' as const, description: 'Article body as simple HTML' },\n excerpt: { type: 'string' as const, description: 'One-line summary shown on cards' },\n language: { type: 'string' as const, description: 'BCP-47 tag, e.g. pt-BR' },\n tags: { type: 'array' as const, items: { type: 'string' as const }, description: 'Topic tags' },\n },\n required: ['title'],\n },\n suggestions: [{ label: 'Escreve um rascunho de post sobre clareamento dental' }],\n permission: { feature: 'blog', action: 'create' as const },\n },\n ],\n // Host owns website nav; no admin nav in M1.\n navigation: [],\n routes: [\n { path: config.basePath, component: BlogListRoute, guard: 'public' },\n { path: `${config.basePath}/:slug`, component: PostDetailRoute, guard: 'public' },\n ],\n widgets: [],\n // Measuring what the posts brought in, and pushing a new one anywhere else.\n integrations: ['google-analytics', 'zapier'],\n // Owns the blog domain tables (categories + posts + anon-read view). The\n // marketing \"Blog\" backoffice writes to these; the website reads them.\n migrations: MIGRATIONS.map((m) => ({\n id: `blog-${m.id}`,\n version: MIGRATION_META[m.id]?.version ?? '1.0.0',\n sql: m.sql,\n description: MIGRATION_META[m.id]?.description ?? m.id,\n })),\n }\n\n return { manifest, Provider, dataProvider: provider }\n}\n\n// Public API\nexport type { BlogPluginOptions, ResolvedBlogConfig } from './config'\nexport type { BlogPost, BlogListQuery, BlogAuthor, BlogCategory } from './types'\nexport type { BlogDataProvider } from './data/types'\nexport { createMockBlogProvider, createSupabaseBlogProvider } from './data'\nexport type { BlogSeed, BlogSeedPost } from './data/mock'\nexport { BlogProvider, useBlogContext } from './context'\nexport type { BlogContextValue } from './context'\nexport { useBlogPosts, usePost } from './hooks'\n\n// SSR seam: the reads as named loaders, resolvable before render, plus the\n// provider registry they read from.\nexport {\n registerBlogLoaders,\n BLOG_POSTS_LOADER,\n BLOG_POST_LOADER,\n} from './data/loaders'\nexport type { BlogPostsParams, BlogPostParams } from './data/loaders'\nexport { setBlogProvider, getBlogProvider, hasBlogProvider, resetBlogProvider } from './data/runtime'\n\n// The declared facts, exported so a connector can build against the contract\n// instead of hard-coding four strings.\nexport { buildBlogDomainEvents, BLOG_POST_EVENT_FIELDS } from './lib/blog-events'\n\n// SEO descriptors + schema.org builders.\nexport { postSeo, blogListSeo, articleJsonLd, blogJsonLd, postUrl, blogUrl } from './lib/seo'\nexport type { BlogSeoDescriptor, BlogSeoOptions } from './lib/seo'\nexport type { UseBlogPostsResult, UsePostResult } from './hooks'\n// The default components (BlogList, PostDetail, PostCard, AuthorAvatar) moved\n// to `@fayz-ai/plugin-blog/components` — they carry react-router-dom, and a\n// static re-export here would put it back into every consumer's bundle.\n"]}
1
+ {"version":3,"sources":["../src/config.ts","../src/data/mock.ts","../src/data/supabase.ts","../src/migrations/index.ts","../src/data/runtime.ts","../src/data/loaders.ts","../src/lib/blog-events.ts","../src/index.ts"],"names":[],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAy5B/B,IAAM,wCAAA,GAA2C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","file":"index.js","sourcesContent":["import type { PluginScope, VerticalId } from '@fayz-ai/core'\nimport type { BlogDataProvider } from './data/types'\nimport type { BlogSeed } from './data/mock'\nimport type { BlogAuthor } from './types'\n\nexport interface BlogPluginOptions {\n /** Base path the plugin mounts its public routes under. Default '/blog'. */\n basePath?: string\n /** Seed posts for the mock provider (used until a real backend is wired). */\n seed?: BlogSeed\n /** Default byline author, applied to any post that doesn't specify its own. */\n defaultAuthor?: BlogAuthor\n /** Publisher/brand name — used for SEO (og:site_name, JSON-LD publisher). */\n siteName?: string\n /** Inject a custom provider (e.g. a CMS). Overrides the safe mock/Supabase resolver. */\n dataProvider?: BlogDataProvider\n /** Tenant whose PUBLISHED posts the public (anon) site reads from Supabase.\n * Required for the Supabase provider on an anon website (mirrors how the\n * public booking plugin takes an explicit tenantId). Without it the safe\n * resolver falls back to the mock/seed provider. */\n tenantId?: string\n /** Section eyebrow / labels for the default list screen. */\n labels?: {\n /** Title shown at the top of the /blog list screen. */\n listTitle?: string\n /** Subtitle under the list title. */\n listSubtitle?: string\n /** \"Back to blog\" link label on the detail screen. */\n backToList?: string\n }\n scope?: PluginScope\n verticalId?: VerticalId\n}\n\nexport interface ResolvedBlogConfig {\n basePath: string\n siteName: string\n labels: {\n listTitle: string\n listSubtitle: string\n backToList: string\n }\n}\n\nexport function resolveConfig(options?: BlogPluginOptions): ResolvedBlogConfig {\n return {\n basePath: options?.basePath ?? '/blog',\n siteName: options?.siteName ?? '',\n labels: {\n listTitle: options?.labels?.listTitle ?? 'Blog',\n listSubtitle: options?.labels?.listSubtitle ?? '',\n backToList: options?.labels?.backToList ?? '← Voltar ao blog',\n },\n }\n}\n","import type { BlogDataProvider } from './types'\nimport type { BlogPost, BlogListQuery, BlogAuthor } from '../types'\n\n/** Slugify a title when a seed post omits an explicit slug. */\nfunction slugify(input: string): string {\n return input\n .normalize('NFD')\n // Drop combining accent marks (and any other non-ASCII) so \"saúde\" -> \"saude\".\n .replace(/[^\\x00-\\x7f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '')\n .slice(0, 80)\n}\n\n/** Seed post — same shape as BlogPost but slug/body/author may be derived. */\nexport type BlogSeedPost = Omit<BlogPost, 'slug' | 'body' | 'author'> & {\n slug?: string\n body?: string\n author?: BlogAuthor\n}\n\nexport interface BlogSeed {\n posts: BlogSeedPost[]\n}\n\nexport interface MockBlogProviderOptions {\n seed?: BlogSeed\n /** Fallback byline used for posts without their own author. */\n defaultAuthor?: BlogAuthor\n}\n\nconst FALLBACK_AUTHOR: BlogAuthor = { name: 'Equipe' }\n\nfunction normalize(post: BlogSeedPost, defaultAuthor: BlogAuthor): BlogPost {\n return {\n ...post,\n slug: post.slug ?? slugify(post.title),\n body: post.body ?? post.excerpt,\n author: post.author ?? defaultAuthor,\n }\n}\n\n/** A built-in fallback post so an unseeded plugin still renders something. */\nconst FALLBACK: BlogSeedPost[] = [\n {\n title: 'Bem-vindo ao blog',\n excerpt: 'Este é um artigo de exemplo servido pelo plugin de blog do Fayz.',\n tag: 'Novidades',\n readTime: '2 min',\n date: 'Hoje',\n image: 'https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=800&q=80',\n },\n]\n\nexport function createMockBlogProvider(options?: MockBlogProviderOptions): BlogDataProvider {\n const defaultAuthor = options?.defaultAuthor ?? FALLBACK_AUTHOR\n const posts: BlogPost[] = (options?.seed?.posts ?? FALLBACK).map((p) => normalize(p, defaultAuthor))\n\n return {\n async listPosts(query?: BlogListQuery): Promise<BlogPost[]> {\n let result = posts\n if (query?.tag) result = result.filter((p) => p.tag === query.tag)\n if (query?.limit != null) result = result.slice(0, query.limit)\n return result\n },\n async getPost(slug: string): Promise<BlogPost | null> {\n return posts.find((p) => p.slug === slug) ?? null\n },\n }\n}\n","import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'\nimport type { BlogDataProvider } from './types'\nimport type { BlogPost, BlogListQuery, BlogAuthor } from '../types'\n\n// ---------------------------------------------------------------------------\n// Supabase-backed blog provider (public/website read side).\n//\n// Reads PUBLISHED posts from the anon-safe view public.v_public_blog_posts\n// (see src/migrations/001_blog.sql §4), scoped to a tenant. The backoffice\n// writes to plg_blog_posts directly via the generic CRUD provider; here we only\n// read the published, column-whitelisted view — safe under the publishable key.\n//\n// The tenant is taken from the configured tenantId (anon site) or, failing that,\n// the active session tenant. Without a client or a tenant we return nothing, and\n// createSafeDataProvider falls back to the mock/seed provider.\n// ---------------------------------------------------------------------------\n\nconst PUBLIC_VIEW = 'v_public_blog_posts'\n\nexport interface SupabaseBlogProviderOptions {\n /** Tenant whose published posts to read (anon website). Defaults to the active session tenant. */\n tenantId?: string\n}\n\nfunction getClient() {\n const supabase = getSupabaseClientOptional() as {\n from: (t: string) => any\n } | null\n if (!supabase) throw new Error('[plugin-blog] Supabase client not initialized')\n return supabase\n}\n\n/** Human month-year label (pt-BR) derived from an ISO date, e.g. \"Janeiro 2025\". */\nfunction humanDate(iso?: string | null): string {\n if (!iso) return ''\n const d = new Date(iso)\n if (Number.isNaN(d.getTime())) return ''\n const label = d.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\nfunction toPost(row: Record<string, any>): BlogPost {\n const author: BlogAuthor = {\n name: row.author_name ?? 'Equipe',\n avatarUrl: row.author_avatar_url ?? undefined,\n role: row.author_role ?? undefined,\n bio: row.author_bio ?? undefined,\n }\n return {\n slug: row.slug,\n title: row.title,\n excerpt: row.excerpt ?? '',\n body: row.body ?? '',\n tag: row.tag ?? '',\n readTime: row.read_time ?? '',\n date: humanDate(row.published_at),\n publishedAt: row.published_at ?? undefined,\n image: row.cover_image ?? '',\n author,\n }\n}\n\nexport function createSupabaseBlogProvider(options?: SupabaseBlogProviderOptions): BlogDataProvider {\n const resolveTenantId = (): string | undefined => options?.tenantId ?? getActiveTenantId() ?? undefined\n\n return {\n async listPosts(query?: BlogListQuery): Promise<BlogPost[]> {\n const tenantId = resolveTenantId()\n if (!tenantId) return []\n let q = getClient()\n .from(PUBLIC_VIEW)\n .select('*')\n .eq('tenant_id', tenantId)\n .order('published_at', { ascending: false, nullsFirst: false })\n if (query?.tag) q = q.eq('tag', query.tag)\n if (query?.limit != null) q = q.limit(query.limit)\n const { data, error } = await q\n if (error) throw error\n return (data ?? []).map(toPost)\n },\n\n async getPost(slug: string): Promise<BlogPost | null> {\n const tenantId = resolveTenantId()\n if (!tenantId) return null\n const { data, error } = await getClient()\n .from(PUBLIC_VIEW)\n .select('*')\n .eq('tenant_id', tenantId)\n .eq('slug', slug)\n .maybeSingle()\n if (error) throw error\n return data ? toPost(data) : null\n },\n }\n}\n","// AUTO-GENERATED from 000_baseline.sql, 001_the_author_is_a_membership.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_000_BASELINE = `-- ============================================================================\n-- plugins/plugin-blog/src/migrations/000_baseline.sql — what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 2 migration files. They are kept, unapplied, in\n-- migrations.archive/ — a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- ── default privileges: reset ───────────────────────────────────────────\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\n\n-- ── table ───────────────────────────────────────────────────────────────\n\nCREATE TABLE public.plg_blog_categories (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n slug text NOT NULL,\n description text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE public.plg_blog_posts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n category_id uuid,\n slug text NOT NULL,\n title text NOT NULL,\n excerpt text DEFAULT ''::text NOT NULL,\n body text DEFAULT ''::text NOT NULL,\n cover_image text,\n author_name text,\n author_role text,\n author_avatar_url text,\n author_bio text,\n status text DEFAULT 'draft'::text NOT NULL,\n read_time text,\n published_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n meta_title text,\n meta_description text,\n canonical_url text,\n og_image text,\n noindex boolean DEFAULT false NOT NULL,\n external_ref text,\n source text DEFAULT 'admin'::text NOT NULL,\n language text,\n tags text[],\n author_member_id uuid,\n CONSTRAINT plg_blog_posts_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'published'::text])))\n);\n\n\n-- ── function ────────────────────────────────────────────────────────────\n\nCREATE FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_denial jsonb;\n v_used int;\n v_title text;\n v_base text;\n v_slug text;\n v_n int := 1;\n v_id uuid;\nBEGIN\n v_title := nullif(btrim(p_payload->>'title'), '');\n IF v_title IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'error', 'title is required to draft a post');\n END IF;\n\n SELECT count(*) INTO v_used FROM public.plg_blog_posts p WHERE p.tenant_id = p_tenant_id;\n v_denial := public.agent_guard(p_tenant_id, p_actor_user_id, 'blog', 'create', 'blog_posts', v_used, 1);\n IF v_denial IS NOT NULL THEN\n RETURN jsonb_build_object('ok', false, 'denial', v_denial);\n END IF;\n\n v_base := coalesce(nullif(public.plg_blog_slugify(v_title), ''), 'post-' || left(md5(v_title), 8));\n v_slug := left(v_base, 120);\n WHILE EXISTS (SELECT 1 FROM public.plg_blog_posts p\n WHERE p.tenant_id = p_tenant_id AND p.slug = v_slug) LOOP\n v_n := v_n + 1;\n IF v_n > 50 THEN\n RETURN jsonb_build_object('ok', false, 'error', format('cannot derive a free slug from %L', v_base));\n END IF;\n v_slug := left(v_base, 112) || '-' || v_n;\n END LOOP;\n\n INSERT INTO public.plg_blog_posts (\n tenant_id, slug, title, excerpt, body, status, source, language, tags, updated_at)\n VALUES (\n p_tenant_id, v_slug, v_title,\n left(btrim(regexp_replace(coalesce(p_payload->>'excerpt', ''), '<[^>]*>', '', 'g')), 500),\n -- Same sanitiser as the external door: the model's output is not more\n -- trustworthy than a partner's, it is just closer to home.\n public.plg_blog_sanitize_html(p_payload->>'body_html'),\n 'draft', 'agent',\n nullif(btrim(p_payload->>'language'), ''),\n CASE WHEN jsonb_typeof(p_payload->'tags') = 'array'\n THEN ARRAY(SELECT btrim(x) FROM jsonb_array_elements_text(p_payload->'tags') AS x WHERE btrim(x) <> '')\n END,\n now())\n RETURNING id INTO v_id;\n\n INSERT INTO public.audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)\n VALUES (p_tenant_id, p_actor_user_id, 'agent.draftBlogPost', 'blog_post', v_id::text,\n jsonb_build_object('title', v_title, 'slug', v_slug));\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_id,\n 'record', jsonb_build_object(\n 'ref', jsonb_build_object('id', v_id, 'resource', 'plg_blog_posts', 'entityKey', 'blog:posts'),\n 'name', v_title,\n 'slug', v_slug,\n 'status', 'draft'));\nEND;\n$$;\n\nCREATE FUNCTION public.blog_upsert_external_post(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_claimed uuid;\n v_ref text;\n v_title text;\n v_base text;\n v_slug text;\n v_status text;\n v_source text;\n v_lang text;\n v_body text;\n v_excerpt text;\n v_tags text[];\n v_author uuid;\n v_pub timestamptz;\n v_cover text;\n v_existing_id uuid;\n v_existing_pub timestamptz;\n v_existing_slug text;\n v_n int := 1;\n v_id uuid;\n v_created boolean := false;\nBEGIN\n -- ── tenant: from the credential, never from the payload ──────────────────\n -- A tenant id the caller supplies is how one tenant writes into another's\n -- blog, and the caller is a machine outside our trust boundary. The only\n -- statement about identity we can believe is the one the credential makes.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF v_tenants IS NULL OR array_length(v_tenants, 1) <> 1 THEN\n RAISE EXCEPTION\n 'blog_upsert_external_post: the caller must present a credential scoped to exactly one tenant'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n v_tenant := v_tenants[1];\n\n -- Refusing beats ignoring: a publisher that believes it is writing into\n -- another tenant must be told so, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL THEN\n BEGIN\n v_claimed := (p_payload->>'tenant_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_claimed := NULL;\n END;\n IF v_claimed IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'blog_upsert_external_post: payload names another tenant; the tenant comes from the credential'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n END IF;\n\n -- ── the article ──────────────────────────────────────────────────────────\n v_ref := nullif(btrim(p_payload->>'external_ref'), '');\n v_title := nullif(btrim(p_payload->>'title'), '');\n IF v_title IS NULL THEN\n RAISE EXCEPTION 'blog_upsert_external_post: title is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n v_status := lower(coalesce(nullif(btrim(p_payload->>'status'), ''), 'draft'));\n IF v_status NOT IN ('draft', 'published') THEN\n RAISE EXCEPTION 'blog_upsert_external_post: unknown status %; expected draft or published', v_status\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n -- Never taken as 'admin' by default: a post that arrived through this door is\n -- external by definition, and mislabelling it is what makes a connector echo\n -- it back to the system that sent it.\n v_source := lower(coalesce(nullif(btrim(p_payload->>'source'), ''), 'external'));\n v_lang := nullif(btrim(p_payload->>'language'), '');\n v_body := public.plg_blog_sanitize_html(p_payload->>'body_html');\n v_excerpt := left(btrim(regexp_replace(coalesce(p_payload->>'excerpt', ''), '<[^>]*>', '', 'g')), 500);\n\n IF jsonb_typeof(p_payload->'tags') = 'array' THEN\n SELECT array_agg(btrim(x)) INTO v_tags\n FROM jsonb_array_elements_text(p_payload->'tags') AS x\n WHERE btrim(x) <> '';\n END IF;\n\n -- An author id is a claim about a person in THIS tenant. Unverified, it would\n -- let an external publisher put a real employee's name on anything it writes.\n IF nullif(btrim(p_payload->>'author_member_id'), '') IS NOT NULL THEN\n BEGIN\n SELECT tm.id INTO v_author\n FROM public.tenant_members tm\n WHERE tm.tenant_id = v_tenant AND tm.id = (p_payload->>'author_member_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_author := NULL;\n END;\n END IF;\n\n v_cover := public.plg_blog_safe_url(\n coalesce(nullif(btrim(p_payload->>'cover_image'), ''), nullif(btrim(p_payload->>'og_image'), '')));\n\n -- ── identity + slug ──────────────────────────────────────────────────────\n IF v_ref IS NOT NULL THEN\n SELECT p.id, p.published_at, p.slug INTO v_existing_id, v_existing_pub, v_existing_slug\n FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.external_ref = v_ref;\n END IF;\n\n -- On re-delivery the existing slug WINS over one re-derived from the title.\n -- A published post's slug is its URL: silently re-slugging it because the\n -- publisher fixed a typo in the headline breaks every inbound link — on an\n -- SEO product's own articles, of all things. Changing it stays possible, but\n -- only by saying so explicitly in \\`slug\\`.\n v_base := coalesce(\n nullif(public.plg_blog_slugify(p_payload->>'slug'), ''),\n nullif(v_existing_slug, ''),\n nullif(public.plg_blog_slugify(v_title), ''),\n 'post-' || left(md5(coalesce(v_ref, gen_random_uuid()::text)), 8));\n v_base := left(v_base, 120);\n\n -- Slug is unique per tenant (001). Two different articles that slugify the\n -- same must not make the second delivery fail — a suffixed slug is a smaller\n -- surprise than a rejected publish.\n v_slug := v_base;\n WHILE EXISTS (\n SELECT 1 FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.slug = v_slug\n AND (v_existing_id IS NULL OR p.id <> v_existing_id)\n ) LOOP\n v_n := v_n + 1;\n IF v_n > 50 THEN\n RAISE EXCEPTION 'blog_upsert_external_post: cannot derive a free slug from %', v_base\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n v_slug := left(v_base, 112) || '-' || v_n;\n END LOOP;\n\n BEGIN\n v_pub := nullif(btrim(p_payload->>'published_at'), '')::timestamptz;\n EXCEPTION WHEN OTHERS THEN\n v_pub := NULL;\n END;\n -- A post that goes back to draft keeps the date it first went out; the public\n -- view filters on status, so nothing leaks, and the history stays true.\n v_pub := coalesce(v_pub, v_existing_pub, CASE WHEN v_status = 'published' THEN now() END);\n\n -- ── write ────────────────────────────────────────────────────────────────\n IF v_existing_id IS NOT NULL THEN\n UPDATE public.plg_blog_posts SET\n slug = v_slug,\n title = v_title,\n excerpt = v_excerpt,\n body = v_body,\n status = v_status,\n published_at = v_pub,\n source = v_source,\n language = v_lang,\n tags = v_tags,\n author_member_id = coalesce(v_author, author_member_id),\n cover_image = coalesce(v_cover, cover_image),\n meta_title = nullif(btrim(p_payload->>'meta_title'), ''),\n meta_description = nullif(btrim(p_payload->>'meta_description'), ''),\n canonical_url = public.plg_blog_safe_url(p_payload->>'canonical_url'),\n og_image = public.plg_blog_safe_url(p_payload->>'og_image'),\n noindex = coalesce((p_payload->>'noindex')::boolean, false),\n updated_at = now()\n WHERE id = v_existing_id;\n v_id := v_existing_id;\n ELSE\n -- ON CONFLICT on the partial index, not just the SELECT above: two\n -- deliveries of the same article racing each other would both find nothing\n -- and both insert. The index is what makes the second one an update.\n INSERT INTO public.plg_blog_posts (\n tenant_id, slug, title, excerpt, body, status, published_at,\n external_ref, source, language, tags, author_member_id, cover_image,\n meta_title, meta_description, canonical_url, og_image, noindex, updated_at)\n VALUES (\n v_tenant, v_slug, v_title, v_excerpt, v_body, v_status, v_pub,\n v_ref, v_source, v_lang, v_tags, v_author, v_cover,\n nullif(btrim(p_payload->>'meta_title'), ''),\n nullif(btrim(p_payload->>'meta_description'), ''),\n public.plg_blog_safe_url(p_payload->>'canonical_url'),\n public.plg_blog_safe_url(p_payload->>'og_image'),\n coalesce((p_payload->>'noindex')::boolean, false),\n now())\n ON CONFLICT (tenant_id, external_ref) WHERE external_ref IS NOT NULL\n DO UPDATE SET\n title = excluded.title,\n excerpt = excluded.excerpt,\n body = excluded.body,\n status = excluded.status,\n published_at = coalesce(excluded.published_at, plg_blog_posts.published_at),\n source = excluded.source,\n language = excluded.language,\n tags = excluded.tags,\n cover_image = coalesce(excluded.cover_image, plg_blog_posts.cover_image),\n meta_title = excluded.meta_title,\n meta_description = excluded.meta_description,\n canonical_url = excluded.canonical_url,\n og_image = excluded.og_image,\n noindex = excluded.noindex,\n updated_at = now()\n RETURNING id, (xmax = 0) INTO v_id, v_created;\n END IF;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_id,\n 'slug', v_slug,\n 'status', v_status,\n 'source', v_source,\n 'external_ref', v_ref,\n 'created', coalesce(v_created, false));\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_emit_post_event() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_row jsonb;\n v_tenant uuid;\n v_id text;\n v_payload jsonb;\nBEGIN\n -- A save that changed nothing is not a fact; without this any bulk re-save\n -- buries the real updates.\n IF TG_OP = 'UPDATE' AND OLD IS NOT DISTINCT FROM NEW THEN\n RETURN NULL;\n END IF;\n\n IF TG_OP = 'DELETE' THEN\n v_row := to_jsonb(OLD);\n ELSE\n v_row := to_jsonb(NEW);\n END IF;\n v_tenant := nullif(v_row->>'tenant_id', '')::uuid;\n v_id := v_row->>'id';\n -- The contract every consumer reads. Not the row: a whole-row payload puts\n -- the article body inside every webhook by default, and whoever needs it\n -- fetches by id.\n v_payload := jsonb_build_object(\n 'post_id', v_id,\n 'slug', v_row->>'slug',\n 'title', v_row->>'title',\n 'status', v_row->>'status',\n 'source', coalesce(v_row->>'source', 'admin'),\n 'external_ref', v_row->>'external_ref',\n 'language', v_row->>'language');\n\n BEGIN\n IF TG_OP = 'INSERT' THEN\n PERFORM public.plg_emit_event('blog.post.created', v_payload, 'blog_post', v_id, v_tenant, NULL);\n IF NEW.status = 'published' THEN\n PERFORM public.plg_emit_event('blog.post.published', v_payload, 'blog_post', v_id, v_tenant, NULL);\n END IF;\n ELSIF TG_OP = 'UPDATE' THEN\n PERFORM public.plg_emit_event('blog.post.updated', v_payload, 'blog_post', v_id, v_tenant, NULL);\n IF NEW.status = 'published' AND OLD.status IS DISTINCT FROM 'published' THEN\n PERFORM public.plg_emit_event('blog.post.published', v_payload, 'blog_post', v_id, v_tenant, NULL);\n END IF;\n ELSE\n PERFORM public.plg_emit_event('blog.post.deleted', v_payload, 'blog_post', v_id, v_tenant, NULL);\n END IF;\n EXCEPTION WHEN OTHERS THEN\n -- Observability must never be able to reject the write that caused it.\n RAISE WARNING '[plg_blog_emit_post_event] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;\n END;\n\n RETURN NULL;\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) RETURNS text\n LANGUAGE sql IMMUTABLE\n AS $$\n SELECT coalesce(\n substring(p_tag from '(?i)\\\\s' || p_name || '\\\\s*=\\\\s*\"([^\"]*)\"'),\n substring(p_tag from '(?i)\\\\s' || p_name || '\\\\s*=\\\\s*''([^'']*)'''),\n substring(p_tag from '(?i)\\\\s' || p_name || '\\\\s*=\\\\s*([^\\\\s\">]+)')\n );\n$$;\n\nCREATE FUNCTION public.plg_blog_html_escape(p_text text) RETURNS text\n LANGUAGE sql IMMUTABLE\n AS $$\n SELECT replace(replace(replace(coalesce(p_text, ''), '<', '&lt;'), '>', '&gt;'), '\"', '&quot;');\n$$;\n\nCREATE FUNCTION public.plg_blog_safe_url(p_url text) RETURNS text\n LANGUAGE plpgsql IMMUTABLE\n AS $$\nDECLARE\n v text;\nBEGIN\n IF p_url IS NULL THEN RETURN NULL; END IF;\n -- Whitespace and control characters go BEFORE the scheme test: \\`java\\\\tscript:\\`\n -- is a working URL in every browser and would pass a naive prefix check.\n v := regexp_replace(btrim(p_url), '[[:space:][:cntrl:]]', '', 'g');\n IF v = '' THEN RETURN NULL; END IF;\n -- Allowlist of schemes. \\`data:\\` is absent on purpose: a data URL is a way to\n -- ship a whole document — including an SVG that scripts — inside an href.\n IF v ~* '^(https?://|mailto:|/|#)' THEN\n RETURN public.plg_blog_html_escape(v);\n END IF;\n RETURN NULL;\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_sanitize_html(p_html text) RETURNS text\n LANGUAGE plpgsql IMMUTABLE\n SET search_path TO 'public'\n AS $$\nDECLARE\n -- The allowlist is what an ARTICLE is made of. Layout tags (div, span,\n -- section, main) are deliberately out: they carry nothing a blog post needs\n -- and every one of them is a place to hang an attribute. Anything not listed\n -- loses its tag and keeps its text, so a stripped wrapper degrades to prose\n -- rather than to a hole in the article.\n c_allowed constant text[] := ARRAY[\n 'p','br','hr','h1','h2','h3','h4','h5','h6',\n 'strong','b','em','i','u','s','sup','sub','code','pre','blockquote','q','cite',\n 'ul','ol','li','dl','dt','dd',\n 'a','img','figure','figcaption',\n 'table','thead','tbody','tfoot','tr','th','td','caption'\n ];\n c_void constant text[] := ARRAY['br','hr','img'];\n -- Elements whose CONTENT is code rather than text.\n c_active constant text :=\n 'script|style|iframe|object|embed|applet|form|noscript|svg|math|template|frameset|frame';\n v text;\n v_rest text;\n v_out text := '';\n v_lt int;\n v_gt int;\n v_tag text;\n v_name text;\n v_val text;\n v_attrs text;\nBEGIN\n IF p_html IS NULL OR btrim(p_html) = '' THEN RETURN ''; END IF;\n v := p_html;\n\n -- Comments first: a conditional comment is executable markup in older\n -- engines, and \\`<!-- <script>… -->\\` would otherwise reach the pass below\n -- already commented out and survive it.\n v := regexp_replace(v, '<!--.*?-->', '', 'g');\n v := regexp_replace(v, '<!\\\\[CDATA\\\\[.*?\\\\]\\\\]>', '', 'g');\n v := regexp_replace(v, '<\\\\?.*?\\\\?>', '', 'g');\n\n -- Active elements go whole. Dropping only the tag would leave the script body\n -- standing as visible page text — the article would read as vandalised even\n -- though nothing executed.\n -- Two Postgres regex traps, both of which fail OPEN and were caught by\n -- running this against a container rather than by reading it:\n -- \\`\\\\y\\`, not \\`\\\\b\\` — in a Postgres RE \\`\\\\b\\` is the BACKSPACE character, so\n -- \\`<script\\\\b\\` matches nothing and the script body survives as page text.\n -- Every quantifier non-greedy — a Postgres RE takes its greediness from its\n -- FIRST quantifier, so one greedy \\`\\\\s*\\` at the front makes the trailing\n -- \\`.*?\\` greedy too, and the first <script> swallows the article down to the\n -- last closing tag in the document.\n v := regexp_replace(v, '<\\\\s*?(' || c_active || ')\\\\y[^>]*?>.*?<\\\\s*?/\\\\s*?\\\\1\\\\s*?>', '', 'gi');\n -- …and the unbalanced halves of the same set, plus the document-level tags\n -- that have no business inside an article body.\n v := regexp_replace(\n v,\n '<\\\\s*/?\\\\s*(' || c_active || '|link|meta|base|title|head|body|html)\\\\y[^>]*>',\n '', 'gi');\n\n -- Walk the remaining tags. A rewrite rather than a filter: every tag that\n -- comes out the other side was BUILT here from a name and an allowlisted\n -- attribute, so nothing an attacker wrote is copied through by accident.\n v_rest := v;\n LOOP\n v_lt := position('<' in v_rest);\n IF v_lt = 0 THEN\n v_out := v_out || v_rest;\n EXIT;\n END IF;\n v_out := v_out || substr(v_rest, 1, v_lt - 1);\n v_rest := substr(v_rest, v_lt);\n\n v_gt := position('>' in v_rest);\n IF v_gt = 0 THEN\n -- An unterminated '<' is text. It is escaped so it cannot open a tag when\n -- something is concatenated after this body.\n v_out := v_out || replace(v_rest, '<', '&lt;');\n EXIT;\n END IF;\n v_tag := substr(v_rest, 1, v_gt);\n v_rest := substr(v_rest, v_gt + 1);\n\n v_name := lower(coalesce(substring(v_tag from '^<\\\\s*/?\\\\s*([a-zA-Z][a-zA-Z0-9]*)'), ''));\n IF v_name = '' OR NOT (v_name = ANY (c_allowed)) THEN\n CONTINUE; -- tag dropped, its text kept\n END IF;\n\n IF v_tag ~ '^<\\\\s*/' THEN\n IF NOT (v_name = ANY (c_void)) THEN v_out := v_out || '</' || v_name || '>'; END IF;\n CONTINUE;\n END IF;\n\n -- Attributes are an allowlist too, and a per-tag one: \\`href\\` on <a>,\n -- \\`src\\`/\\`alt\\` on <img>. Everything else — style, on*, srcset, class,\n -- data-* — is dropped rather than inspected, because inspecting means\n -- maintaining a list of what is dangerous, and that list is never finished.\n v_attrs := '';\n IF v_name = 'a' THEN\n v_val := public.plg_blog_safe_url(public.plg_blog_html_attr(v_tag, 'href'));\n IF v_val IS NOT NULL THEN\n -- An external publisher's outbound links are not the tenant's\n -- endorsement, and an unmarked link farm is precisely the SEO damage\n -- this seam could otherwise cause on the tenant's own domain.\n v_attrs := ' href=\"' || v_val || '\" rel=\"nofollow noopener\"';\n END IF;\n ELSIF v_name = 'img' THEN\n v_val := public.plg_blog_safe_url(public.plg_blog_html_attr(v_tag, 'src'));\n IF v_val IS NULL THEN CONTINUE; END IF; -- an <img> with no safe src is nothing\n v_attrs := ' src=\"' || v_val || '\" alt=\"'\n || public.plg_blog_html_escape(public.plg_blog_html_attr(v_tag, 'alt')) || '\"';\n END IF;\n\n v_out := v_out || '<' || v_name || v_attrs\n || CASE WHEN v_name = ANY (c_void) THEN ' />' ELSE '>' END;\n END LOOP;\n\n RETURN v_out;\nEND;\n$$;\n\nCREATE FUNCTION public.plg_blog_slugify(p_text text) RETURNS text\n LANGUAGE sql IMMUTABLE\n AS $$\n SELECT btrim(\n regexp_replace(\n translate(lower(coalesce(p_text, '')),\n 'áàâãäåéèêëíìîïóòôõöøúùûüçñýÿ',\n 'aaaaaaeeeeiiiioooooouuuucnyy'),\n '[^a-z0-9]+', '-', 'g'),\n '-');\n$$;\n\n\n-- ── constraint ──────────────────────────────────────────────────────────\n\nALTER TABLE ONLY public.plg_blog_categories\n ADD CONSTRAINT plg_blog_categories_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_pkey PRIMARY KEY (id);\n\n\n-- ── view ────────────────────────────────────────────────────────────────\n\nCREATE VIEW public.v_public_blog_posts AS\n SELECT p.id,\n p.tenant_id,\n p.slug,\n p.title,\n p.excerpt,\n p.body,\n COALESCE(c.name, ''::text) AS tag,\n p.cover_image,\n p.author_name,\n p.author_role,\n p.author_avatar_url,\n p.author_bio,\n p.read_time,\n p.published_at,\n p.meta_title,\n p.meta_description,\n p.canonical_url,\n p.og_image,\n p.noindex,\n p.language,\n p.tags\n FROM (public.plg_blog_posts p\n LEFT JOIN public.plg_blog_categories c ON ((c.id = p.category_id)))\n WHERE (p.status = 'published'::text);\n\n\n-- ── index ───────────────────────────────────────────────────────────────\n\nCREATE INDEX idx_plg_blog_categories_tenant ON public.plg_blog_categories USING btree (tenant_id);\n\nCREATE INDEX idx_plg_blog_posts_category ON public.plg_blog_posts USING btree (category_id);\n\nCREATE INDEX idx_plg_blog_posts_source ON public.plg_blog_posts USING btree (tenant_id, source, updated_at DESC);\n\nCREATE INDEX idx_plg_blog_posts_status ON public.plg_blog_posts USING btree (tenant_id, status, published_at DESC);\n\nCREATE INDEX idx_plg_blog_posts_tenant ON public.plg_blog_posts USING btree (tenant_id);\n\nCREATE UNIQUE INDEX uq_plg_blog_categories_tenant_slug ON public.plg_blog_categories USING btree (tenant_id, slug);\n\nCREATE UNIQUE INDEX uq_plg_blog_posts_tenant_external_ref ON public.plg_blog_posts USING btree (tenant_id, external_ref) WHERE (external_ref IS NOT NULL);\n\nCREATE UNIQUE INDEX uq_plg_blog_posts_tenant_slug ON public.plg_blog_posts USING btree (tenant_id, slug);\n\n\n-- ── fk constraint ───────────────────────────────────────────────────────\n\nALTER TABLE ONLY public.plg_blog_categories\n ADD CONSTRAINT plg_blog_categories_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_author_member_id_fkey FOREIGN KEY (author_member_id) REFERENCES public.tenant_members(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.plg_blog_categories(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\n\n-- ── trigger ─────────────────────────────────────────────────────────────\n\nCREATE TRIGGER plg_blog_posts_emit_facts AFTER INSERT OR DELETE OR UPDATE ON public.plg_blog_posts FOR EACH ROW EXECUTE FUNCTION public.plg_blog_emit_post_event();\n\n\n-- ── row security ────────────────────────────────────────────────────────\n\nALTER TABLE public.plg_blog_categories ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_blog_posts ENABLE ROW LEVEL SECURITY;\n\n\n-- ── policy ──────────────────────────────────────────────────────────────\n\nCREATE POLICY plg_blog_categories_delete ON public.plg_blog_categories FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_categories_insert ON public.plg_blog_categories FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_categories_select ON public.plg_blog_categories FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_categories_update ON public.plg_blog_categories FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_delete ON public.plg_blog_posts FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_insert ON public.plg_blog_posts FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_select ON public.plg_blog_posts FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_blog_posts_update ON public.plg_blog_posts FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\n\n-- ── acl ─────────────────────────────────────────────────────────────────\n\nREVOKE ALL ON FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.agent_blog_draft_post(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.blog_upsert_external_post(p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.blog_upsert_external_post(p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.blog_upsert_external_post(p_payload jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_emit_post_event() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_emit_post_event() TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_emit_post_event() TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_html_attr(p_tag text, p_name text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_html_escape(p_text text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_html_escape(p_text text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_html_escape(p_text text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_safe_url(p_url text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_safe_url(p_url text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_safe_url(p_url text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_sanitize_html(p_html text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_sanitize_html(p_html text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_sanitize_html(p_html text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_blog_slugify(p_text text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_blog_slugify(p_text text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_blog_slugify(p_text text) TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_blog_categories TO anon;\nGRANT ALL ON TABLE public.plg_blog_categories TO authenticated;\nGRANT ALL ON TABLE public.plg_blog_categories TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_blog_posts TO anon;\nGRANT ALL ON TABLE public.plg_blog_posts TO authenticated;\nGRANT ALL ON TABLE public.plg_blog_posts TO service_role;\n\nGRANT SELECT,MAINTAIN ON TABLE public.v_public_blog_posts TO anon;\nGRANT ALL ON TABLE public.v_public_blog_posts TO authenticated;\nGRANT ALL ON TABLE public.v_public_blog_posts TO service_role;\n\n\n-- ── default privileges: restore ─────────────────────────────────────────\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n`\n\nexport const MIGRATION_001_THE_AUTHOR_IS_A_MEMBERSHIP = `-- ============================================================================\n-- 001_the_author_is_a_membership — the FK and the lookup leave the projection\n-- (#214 / #352).\n--\n-- plg_blog_posts.author_member_id pointed at public.tenant_members(id), which\n-- is a COMPAT PROJECTION of app.memberships maintained by triggers — so a real\n-- foreign key was resting on a derived table. That is the reason the projection\n-- could not be dropped, and it is a stronger reason than \"a screen reads it\":\n-- Postgres refuses to drop a table another table references.\n--\n-- The move is safe precisely because the projection's own comment says the ids\n-- are the same (\"COMPAT PROJECTION of app.memberships (#134): same ids\"), so\n-- every existing author_member_id already names an app.memberships row.\n-- ============================================================================\n\nALTER TABLE public.plg_blog_posts DROP CONSTRAINT IF EXISTS plg_blog_posts_author_member_id_fkey;\nALTER TABLE public.plg_blog_posts\n ADD CONSTRAINT plg_blog_posts_author_member_id_fkey\n FOREIGN KEY (author_member_id) REFERENCES app.memberships(id) ON DELETE SET NULL;\n\nCREATE OR REPLACE FUNCTION public.blog_upsert_external_post(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_claimed uuid;\n v_ref text;\n v_title text;\n v_base text;\n v_slug text;\n v_status text;\n v_source text;\n v_lang text;\n v_body text;\n v_excerpt text;\n v_tags text[];\n v_author uuid;\n v_pub timestamptz;\n v_cover text;\n v_existing_id uuid;\n v_existing_pub timestamptz;\n v_existing_slug text;\n v_n int := 1;\n v_id uuid;\n v_created boolean := false;\nBEGIN\n -- ── tenant: from the credential, never from the payload ──────────────────\n -- A tenant id the caller supplies is how one tenant writes into another's\n -- blog, and the caller is a machine outside our trust boundary. The only\n -- statement about identity we can believe is the one the credential makes.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF v_tenants IS NULL OR array_length(v_tenants, 1) <> 1 THEN\n RAISE EXCEPTION\n 'blog_upsert_external_post: the caller must present a credential scoped to exactly one tenant'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n v_tenant := v_tenants[1];\n\n -- Refusing beats ignoring: a publisher that believes it is writing into\n -- another tenant must be told so, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL THEN\n BEGIN\n v_claimed := (p_payload->>'tenant_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_claimed := NULL;\n END;\n IF v_claimed IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'blog_upsert_external_post: payload names another tenant; the tenant comes from the credential'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n END IF;\n\n -- ── the article ──────────────────────────────────────────────────────────\n v_ref := nullif(btrim(p_payload->>'external_ref'), '');\n v_title := nullif(btrim(p_payload->>'title'), '');\n IF v_title IS NULL THEN\n RAISE EXCEPTION 'blog_upsert_external_post: title is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n v_status := lower(coalesce(nullif(btrim(p_payload->>'status'), ''), 'draft'));\n IF v_status NOT IN ('draft', 'published') THEN\n RAISE EXCEPTION 'blog_upsert_external_post: unknown status %; expected draft or published', v_status\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n -- Never taken as 'admin' by default: a post that arrived through this door is\n -- external by definition, and mislabelling it is what makes a connector echo\n -- it back to the system that sent it.\n v_source := lower(coalesce(nullif(btrim(p_payload->>'source'), ''), 'external'));\n v_lang := nullif(btrim(p_payload->>'language'), '');\n v_body := public.plg_blog_sanitize_html(p_payload->>'body_html');\n v_excerpt := left(btrim(regexp_replace(coalesce(p_payload->>'excerpt', ''), '<[^>]*>', '', 'g')), 500);\n\n IF jsonb_typeof(p_payload->'tags') = 'array' THEN\n SELECT array_agg(btrim(x)) INTO v_tags\n FROM jsonb_array_elements_text(p_payload->'tags') AS x\n WHERE btrim(x) <> '';\n END IF;\n\n -- An author id is a claim about a person in THIS tenant. Unverified, it would\n -- let an external publisher put a real employee's name on anything it writes.\n IF nullif(btrim(p_payload->>'author_member_id'), '') IS NOT NULL THEN\n BEGIN\n SELECT tm.id INTO v_author\n FROM app.memberships tm\n WHERE tm.tenant_id = v_tenant AND tm.id = (p_payload->>'author_member_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n v_author := NULL;\n END;\n END IF;\n\n v_cover := public.plg_blog_safe_url(\n coalesce(nullif(btrim(p_payload->>'cover_image'), ''), nullif(btrim(p_payload->>'og_image'), '')));\n\n -- ── identity + slug ──────────────────────────────────────────────────────\n IF v_ref IS NOT NULL THEN\n SELECT p.id, p.published_at, p.slug INTO v_existing_id, v_existing_pub, v_existing_slug\n FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.external_ref = v_ref;\n END IF;\n\n -- On re-delivery the existing slug WINS over one re-derived from the title.\n -- A published post's slug is its URL: silently re-slugging it because the\n -- publisher fixed a typo in the headline breaks every inbound link — on an\n -- SEO product's own articles, of all things. Changing it stays possible, but\n -- only by saying so explicitly in \\`slug\\`.\n v_base := coalesce(\n nullif(public.plg_blog_slugify(p_payload->>'slug'), ''),\n nullif(v_existing_slug, ''),\n nullif(public.plg_blog_slugify(v_title), ''),\n 'post-' || left(md5(coalesce(v_ref, gen_random_uuid()::text)), 8));\n v_base := left(v_base, 120);\n\n -- Slug is unique per tenant (001). Two different articles that slugify the\n -- same must not make the second delivery fail — a suffixed slug is a smaller\n -- surprise than a rejected publish.\n v_slug := v_base;\n WHILE EXISTS (\n SELECT 1 FROM public.plg_blog_posts p\n WHERE p.tenant_id = v_tenant AND p.slug = v_slug\n AND (v_existing_id IS NULL OR p.id <> v_existing_id)\n ) LOOP\n v_n := v_n + 1;\n IF v_n > 50 THEN\n RAISE EXCEPTION 'blog_upsert_external_post: cannot derive a free slug from %', v_base\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n v_slug := left(v_base, 112) || '-' || v_n;\n END LOOP;\n\n BEGIN\n v_pub := nullif(btrim(p_payload->>'published_at'), '')::timestamptz;\n EXCEPTION WHEN OTHERS THEN\n v_pub := NULL;\n END;\n -- A post that goes back to draft keeps the date it first went out; the public\n -- view filters on status, so nothing leaks, and the history stays true.\n v_pub := coalesce(v_pub, v_existing_pub, CASE WHEN v_status = 'published' THEN now() END);\n\n -- ── write ────────────────────────────────────────────────────────────────\n IF v_existing_id IS NOT NULL THEN\n UPDATE public.plg_blog_posts SET\n slug = v_slug,\n title = v_title,\n excerpt = v_excerpt,\n body = v_body,\n status = v_status,\n published_at = v_pub,\n source = v_source,\n language = v_lang,\n tags = v_tags,\n author_member_id = coalesce(v_author, author_member_id),\n cover_image = coalesce(v_cover, cover_image),\n meta_title = nullif(btrim(p_payload->>'meta_title'), ''),\n meta_description = nullif(btrim(p_payload->>'meta_description'), ''),\n canonical_url = public.plg_blog_safe_url(p_payload->>'canonical_url'),\n og_image = public.plg_blog_safe_url(p_payload->>'og_image'),\n noindex = coalesce((p_payload->>'noindex')::boolean, false),\n updated_at = now()\n WHERE id = v_existing_id;\n v_id := v_existing_id;\n ELSE\n -- ON CONFLICT on the partial index, not just the SELECT above: two\n -- deliveries of the same article racing each other would both find nothing\n -- and both insert. The index is what makes the second one an update.\n INSERT INTO public.plg_blog_posts (\n tenant_id, slug, title, excerpt, body, status, published_at,\n external_ref, source, language, tags, author_member_id, cover_image,\n meta_title, meta_description, canonical_url, og_image, noindex, updated_at)\n VALUES (\n v_tenant, v_slug, v_title, v_excerpt, v_body, v_status, v_pub,\n v_ref, v_source, v_lang, v_tags, v_author, v_cover,\n nullif(btrim(p_payload->>'meta_title'), ''),\n nullif(btrim(p_payload->>'meta_description'), ''),\n public.plg_blog_safe_url(p_payload->>'canonical_url'),\n public.plg_blog_safe_url(p_payload->>'og_image'),\n coalesce((p_payload->>'noindex')::boolean, false),\n now())\n ON CONFLICT (tenant_id, external_ref) WHERE external_ref IS NOT NULL\n DO UPDATE SET\n title = excluded.title,\n excerpt = excluded.excerpt,\n body = excluded.body,\n status = excluded.status,\n published_at = coalesce(excluded.published_at, plg_blog_posts.published_at),\n source = excluded.source,\n language = excluded.language,\n tags = excluded.tags,\n cover_image = coalesce(excluded.cover_image, plg_blog_posts.cover_image),\n meta_title = excluded.meta_title,\n meta_description = excluded.meta_description,\n canonical_url = excluded.canonical_url,\n og_image = excluded.og_image,\n noindex = excluded.noindex,\n updated_at = now()\n RETURNING id, (xmax = 0) INTO v_id, v_created;\n END IF;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_id,\n 'slug', v_slug,\n 'status', v_status,\n 'source', v_source,\n 'external_ref', v_ref,\n 'created', coalesce(v_created, false));\nEND;\n$$;`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n { id: \"001_the_author_is_a_membership\", sql: MIGRATION_001_THE_AUTHOR_IS_A_MEMBERSHIP },\n]\n","import type { BlogDataProvider } from './types'\n\n// ---------------------------------------------------------------------------\n// The active blog provider, reachable outside React.\n//\n// The hooks read the provider from context, which is right for a component but\n// useless to a loader: a loader is a plain function the page resolver calls\n// before (and without) rendering, so it cannot be inside a provider. Registering\n// the provider here is what lets `blog.posts` and `blog.post` be resolved by a\n// server ahead of render — the same shape `@fayz-ai/shop` uses for its own port.\n//\n// One provider per process, set by `createBlogPlugin`. That matches how a blog\n// is mounted (once, at the app root) and how a storefront is served (one store\n// per process). A multi-tenant renderer would need this request-scoped, which is\n// the same open question the shop provider has.\n// ---------------------------------------------------------------------------\n\nlet _provider: BlogDataProvider | null = null\n\nexport function setBlogProvider(provider: BlogDataProvider): void {\n _provider = provider\n}\n\n/**\n * The registered provider.\n *\n * Throws rather than returning an empty result: a loader that silently answered\n * \"no posts\" when the plugin was never mounted would render an empty blog and\n * give no reason, and the caller cannot tell that apart from a blog with no\n * posts yet.\n */\nexport function getBlogProvider(): BlogDataProvider {\n if (!_provider) {\n throw new Error(\n '[plugin-blog] no blog provider registered — call createBlogPlugin() before resolving blog loaders, and mount its Provider at the app root.',\n )\n }\n return _provider\n}\n\nexport function hasBlogProvider(): boolean {\n return _provider !== null\n}\n\n/** Test seam. */\nexport function resetBlogProvider(): void {\n _provider = null\n}\n","import { defineLoader, hasLoader } from '@fayz-ai/core'\nimport { getBlogProvider } from './runtime'\nimport type { BlogPost } from '../types'\n\n// ---------------------------------------------------------------------------\n// The blog's reads, as named loaders.\n//\n// A hook that fetches in an effect can only ever produce a loading state on a\n// server, so a blog rendered headlessly says \"Carregando…\" and a crawler indexes\n// that. Declaring the reads instead puts them in the same registry the shop's\n// loaders live in, which is what the page resolver walks before render and what\n// `renderStore` resolves between passes — so a post's body is in the HTML, and\n// two rails asking for the same list cost one provider call.\n//\n// Registered into core's registry, not the storefront's: a plugin depends on\n// core only, and that is the registry both the storefront resolver and the\n// headless renderer read.\n// ---------------------------------------------------------------------------\n\nexport const BLOG_POSTS_LOADER = 'blog.posts'\nexport const BLOG_POST_LOADER = 'blog.post'\n\nexport interface BlogPostsParams extends Record<string, unknown> {\n /** Cap the list. Omitted means every published post. */\n limit?: number\n /** Only posts carrying this tag. */\n tag?: string\n}\n\nexport interface BlogPostParams extends Record<string, unknown> {\n slug?: string\n}\n\nasync function blogPosts(params: BlogPostsParams): Promise<BlogPost[]> {\n const { limit, tag } = params\n return getBlogProvider().listPosts({ limit, tag })\n}\n\nasync function blogPost(params: BlogPostParams): Promise<BlogPost | null> {\n const slug = typeof params.slug === 'string' ? params.slug : undefined\n // A missing slug is \"no post\", not an error: a route param can legitimately\n // be absent for one render, and throwing would take the page down for it.\n if (!slug) return null\n return getBlogProvider().getPost(slug)\n}\n\n/**\n * Register the blog reads. Idempotent, so mounting a second blog (or a hot\n * reload) does not throw on a duplicate id.\n */\nexport function registerBlogLoaders(): void {\n if (!hasLoader(BLOG_POSTS_LOADER)) {\n defineLoader<BlogPostsParams, BlogPost[]>(BLOG_POSTS_LOADER, blogPosts, {\n source: 'plugin',\n title: 'Blog posts',\n description:\n 'Published posts, newest first. The read behind a blog index, a \"latest articles\" rail, or a tag archive.',\n output:\n 'Array of BlogPost (slug, title, excerpt, body, image, tag, author, publishedAt …).',\n settings: [\n {\n id: 'limit',\n type: 'number',\n label: 'How many',\n help: 'Empty means every published post.',\n min: 1,\n max: 100,\n },\n {\n id: 'tag',\n type: 'text',\n label: 'Tag',\n help: 'Only posts carrying this tag. Empty means all posts.',\n },\n ],\n })\n }\n\n if (!hasLoader(BLOG_POST_LOADER)) {\n defineLoader<BlogPostParams, BlogPost | null>(BLOG_POST_LOADER, blogPost, {\n source: 'plugin',\n title: 'Blog post',\n description: 'One post by slug. The read behind a post page.',\n output: 'A BlogPost, or null when no post has that slug.',\n settings: [\n {\n id: 'slug',\n type: 'text',\n label: 'Slug',\n help: 'Usually bound to the route param, e.g. `{ $param: \"slug\" }`.',\n },\n ],\n })\n }\n}\n","import type { EventField, PluginEventDefinition } from '@fayz-ai/core'\n\n// ---------------------------------------------------------------------------\n// The FACTS of the blog.\n//\n// The emitter is not in this file and cannot be: a post is written by the\n// backoffice, by an external publisher through blog_upsert_external_post, by\n// the agent and occasionally by hand in SQL. The one place all four end up is\n// the row, so the emitter is a trigger on plg_blog_posts (migration 002 §6) and\n// this is its contract.\n//\n// `source` is the field the whole integration turns on. It names who WROTE the\n// post — 'admin', 'ranklayer', 'agent' — and a connector reads it to decide\n// whether the fact is news or its own echo coming back. Without it, publishing\n// an article into the tenant produces an event that sends the article back to\n// the publisher, which sends it again.\n// ---------------------------------------------------------------------------\n\n/** Payload keys, in the order the trigger builds them. */\nexport const BLOG_POST_EVENT_FIELDS: EventField[] = [\n { name: 'post_id', label: 'ID do post', type: 'id', example: '8f14e45f-…' },\n { name: 'slug', label: 'Endereço', type: 'string', example: 'saude-bucal-na-terceira-idade' },\n { name: 'title', label: 'Título', type: 'string', example: 'Saúde bucal na terceira idade' },\n { name: 'status', label: 'Situação', type: 'string', example: 'published' },\n { name: 'source', label: 'Origem', type: 'string', example: 'ranklayer' },\n { name: 'external_ref', label: 'Referência externa', type: 'string', example: 'rl-article-77' },\n { name: 'language', label: 'Idioma', type: 'string', example: 'pt-BR' },\n]\n\nconst PAYLOAD_SCHEMA: Record<string, unknown> = {\n type: 'object',\n properties: {\n post_id: { type: 'string', format: 'uuid', description: 'Post id in the tenant' },\n slug: { type: 'string', description: 'URL segment under the blog base path' },\n title: { type: 'string' },\n status: { type: 'string', enum: ['draft', 'published'] },\n source: {\n type: 'string',\n description:\n \"Who wrote the post — 'admin', 'ranklayer', 'agent'. A connector compares it against its own id to avoid echoing a post back to the system it came from.\",\n },\n external_ref: { type: 'string', description: \"The writing system's own id for the article, when it has one\" },\n language: { type: 'string', description: 'BCP-47 tag, when the writer declares one' },\n },\n required: ['post_id', 'slug', 'title', 'status', 'source'],\n additionalProperties: false,\n}\n\n/**\n * `audit: true` on created/published/deleted: putting an article on the\n * company's public site and taking it down again are ACTS, and \"who published\n * this?\" is the question the trail exists to answer — sharper still when the\n * answer is a partner's connector rather than a person. `updated` stays out:\n * an SEO product rewrites the same article on a schedule, and a trail where\n * every third line is \"post updated\" is one nobody reads.\n */\nexport function buildBlogDomainEvents(): PluginEventDefinition[] {\n return [\n {\n name: 'blog.post.created',\n title: 'Post criado',\n description: 'Um artigo novo entrou no blog',\n trigger: 'Quando um post é criado — pelo painel, pelo assistente ou por um publicador externo',\n icon: 'FilePlus',\n audit: true,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n {\n name: 'blog.post.updated',\n title: 'Post alterado',\n description: 'O conteúdo ou os metadados do artigo mudaram',\n trigger: 'Quando um post é salvo com alguma alteração de verdade',\n icon: 'FilePen',\n audit: false,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n {\n name: 'blog.post.published',\n title: 'Post publicado',\n description: 'O artigo ficou visível no site',\n trigger: 'Quando o post passa a publicado — na criação ou saindo de rascunho',\n icon: 'Globe',\n audit: true,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n {\n name: 'blog.post.deleted',\n title: 'Post excluído',\n description: 'O artigo saiu do ar',\n trigger: 'Quando um post é excluído',\n icon: 'FileX',\n audit: true,\n fields: BLOG_POST_EVENT_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n ]\n}\n","import { createElement, lazy, Suspense, type ComponentType, type FC, type ReactNode } from 'react'\nimport type { PluginManifest } from '@fayz-ai/core'\nimport { createSafeDataProvider } from '@fayz-ai/core'\nimport { resolveConfig, type BlogPluginOptions } from './config'\nimport { createMockBlogProvider } from './data/mock'\nimport { createSupabaseBlogProvider } from './data/supabase'\nimport { BlogProvider, type BlogContextValue } from './context'\nimport { MIGRATIONS } from './migrations'\nimport { setBlogProvider } from './data/runtime'\nimport { registerBlogLoaders } from './data/loaders'\nimport { buildBlogDomainEvents } from './lib/blog-events'\n\n// ---------------------------------------------------------------------------\n// @fayz-ai/plugin-blog — website-surface blog/articles plugin.\n//\n// First real consumer of scaffolds:['website']. Ships public routes (/blog,\n// /blog/:slug) + data hooks (useBlogPosts/usePost). A bespoke host site feeds\n// its own card markup from the hooks and mounts the routes inside its chrome.\n// See createBlogPlugin's return: a { manifest, Provider } bundle the host reads\n// uniformly (manifest.routes for mounting, Provider for the app-root context).\n// ---------------------------------------------------------------------------\n\nconst MIGRATION_META: Record<string, { version: string; description: string }> = {\n '001_blog': {\n version: '1.0.0',\n description: 'Blog content model (plg_blog_categories, plg_blog_posts) + anon-read view',\n },\n '002_external_posts': {\n version: '1.1.0',\n description:\n 'Per-post SEO, external identity (external_ref/source/language/tags), blog_upsert_external_post + agent_blog_draft_post, and the blog.post.* triggers',\n },\n}\n\n// ---------------------------------------------------------------------------\n// The default route components are the ONLY part of this package that knows\n// react-router-dom, and they exist for one kind of host: a website on\n// react-router that mounts `manifest.routes` as they come. Every other\n// consumer — a storefront on the SDK's own router adapter, a TanStack Start\n// host, anything that reads through the hooks or the loaders and draws its\n// own markup — never renders them, and until now still shipped react-router\n// (and @remix-run/router underneath) to every shopper because this entry\n// imported them statically. Measured on the first SSR store: ~8% of the\n// client bundle for a router it does not use.\n//\n// So the manifest carries them LAZILY: the import is deferred to first\n// render, which puts the components and their router into a chunk of their\n// own that a host only downloads when it actually mounts one of these\n// routes. Each carries its own Suspense boundary, so a react-router host that\n// renders `route.component` directly (hempdent, seusorriso) keeps working\n// unchanged. Importing them directly is still possible, from\n// `@fayz-ai/plugin-blog/components`.\n// ---------------------------------------------------------------------------\n\nfunction lazyRoute(load: () => Promise<{ default: ComponentType<unknown> }>): ComponentType<unknown> {\n const Lazy = lazy(load)\n const Route: FC<unknown> = () => createElement(Suspense, { fallback: null }, createElement(Lazy))\n return Route\n}\n\nconst BlogListRoute = lazyRoute(() => import('./components/BlogList').then((m) => ({ default: m.BlogList as ComponentType<unknown> })))\nBlogListRoute.displayName = 'BlogListRoute'\nconst PostDetailRoute = lazyRoute(() => import('./components/PostDetail').then((m) => ({ default: m.PostDetail as ComponentType<unknown> })))\nPostDetailRoute.displayName = 'PostDetailRoute'\n\nexport interface BlogWebsitePlugin {\n /** Standard plugin manifest (routes + metadata). */\n manifest: PluginManifest\n /** App-root context provider, pre-bound to this plugin's resolved provider/config. */\n Provider: FC<{ children: ReactNode }>\n /** The resolved data provider (exposed for advanced host wiring/tests). */\n dataProvider: BlogContextValue['provider']\n}\n\nexport function createBlogPlugin(options?: BlogPluginOptions): BlogWebsitePlugin {\n const config = resolveConfig(options)\n const provider =\n options?.dataProvider ??\n createSafeDataProvider(\n () => createSupabaseBlogProvider({ tenantId: options?.tenantId }),\n () => createMockBlogProvider({ seed: options?.seed, defaultAuthor: options?.defaultAuthor }),\n )\n\n // Reachable outside React, and the reads declared, so a page resolver or a\n // headless render can answer them before anything renders. Without this a\n // server-rendered blog can only emit its loading state.\n setBlogProvider(provider)\n registerBlogLoaders()\n\n const value: BlogContextValue = { provider, config }\n const Provider: FC<{ children: ReactNode }> = ({ children }) =>\n createElement(BlogProvider, { value, children })\n Provider.displayName = 'BlogPluginProvider'\n\n const manifest: PluginManifest = {\n id: 'blog',\n name: 'Blog',\n icon: 'Newspaper',\n // Tracks package.json. It sat at '0.1.0' for nine minor releases, and a\n // partner reading the manifest concluded the plugin was a stub.\n version: '0.10.0',\n apiVersion: 1,\n scope: options?.scope ?? 'universal',\n verticalId: options?.verticalId,\n scaffolds: ['website', 'landing_page'],\n defaultEnabled: true,\n dependencies: [],\n declaredFeatures: [{ id: 'blog', label: 'Blog', group: 'Attract' }],\n // Blog posts count against the plan cap. The create UI lives in the\n // marketing \"Blog\" backoffice, which writes to this plugin's posts table.\n declaredLimits: [{ key: 'blog_posts', label: 'Blog posts', table: 'plg_blog_posts' }],\n // The facts. Emitted by a trigger on the posts table (migration 002 §6), so\n // an external publisher's write produces the same event as the backoffice's.\n events: buildBlogDomainEvents(),\n // The generic row-level ledger coverage (registry.blog_post.*), which is a\n // different vocabulary from the four above on purpose: it says a ROW\n // changed, the events say what happened to a POST. Nothing attaches this\n // yet — plg_attach_event_source is wired in a later milestone.\n emitsFrom: [{ table: 'plg_blog_posts', entityKey: 'blog_post' }],\n declaredRpcs: [\n {\n name: 'blog_upsert_external_post',\n kind: 'write' as const,\n description:\n 'Ingest door for an external publisher (RankLayer). Tenant pinned from the caller credential, idempotent on (tenant_id, external_ref), body_html sanitised against a tag allowlist in SQL.',\n audits: false,\n },\n {\n name: 'agent_blog_draft_post',\n kind: 'write' as const,\n description:\n 'Guarded blog draft: agent_guard (role → plan → blog_posts cap), body sanitised by the same allowlist, always status draft, audited.',\n audits: true,\n },\n ],\n // Without this the blog is invisible to the agent: it carries its own data\n // provider, and only queryEntities reaches the server-plane executor.\n queryEntities: [\n {\n key: 'blog:posts',\n entity: {\n name: 'Post',\n namePlural: 'Posts',\n icon: 'Newspaper',\n permission: { feature: 'blog', action: 'read' },\n fields: [\n { key: 'title', label: 'Título', type: 'text', searchable: true },\n { key: 'slug', label: 'Endereço', type: 'text', searchable: true },\n { key: 'status', label: 'Situação', type: 'text' },\n { key: 'source', label: 'Origem', type: 'text' },\n { key: 'language', label: 'Idioma', type: 'text' },\n { key: 'published_at', label: 'Publicado em', type: 'text' },\n { key: 'updated_at', label: 'Atualizado em', type: 'text' },\n ],\n data: {\n table: 'plg_blog_posts',\n tenantScoped: true,\n searchColumns: ['title', 'slug'],\n },\n },\n },\n ],\n aiTools: [\n {\n id: 'blog.list-posts',\n name: 'listBlogPosts',\n description:\n 'Lists the blog posts of this tenant — title, slug, status, source and dates. Use it to answer what has been published, what is still a draft, and what came from an external publisher.',\n icon: 'Newspaper',\n mode: 'read' as const,\n execution: { plane: 'server' as const, kind: 'entity_read' as const, entity: 'blog:posts' },\n category: 'Blog',\n parameters: {\n type: 'object' as const,\n properties: {\n status: { type: 'string' as const, enum: ['draft', 'published'] },\n search: { type: 'string' as const, description: 'Match against title or slug' },\n },\n },\n suggestions: [{ label: 'Quais posts estão em rascunho?' }, { label: 'O que foi publicado no blog este mês?' }],\n permission: { feature: 'blog', action: 'read' as const },\n },\n {\n id: 'blog.draft-post',\n name: 'draftBlogPost',\n description:\n 'Creates a blog post as a DRAFT — it never goes live from here; a person publishes it from the blog backoffice. Takes a title and the article body as simple HTML (headings, paragraphs, lists, links).',\n icon: 'FilePlus',\n mode: 'persist' as const,\n limitKey: 'blog_posts',\n execution: { plane: 'server' as const, kind: 'rpc' as const, rpc: 'agent_blog_draft_post' },\n category: 'Blog',\n parameters: {\n type: 'object' as const,\n properties: {\n title: { type: 'string' as const, description: 'Post title' },\n body_html: { type: 'string' as const, description: 'Article body as simple HTML' },\n excerpt: { type: 'string' as const, description: 'One-line summary shown on cards' },\n language: { type: 'string' as const, description: 'BCP-47 tag, e.g. pt-BR' },\n tags: { type: 'array' as const, items: { type: 'string' as const }, description: 'Topic tags' },\n },\n required: ['title'],\n },\n suggestions: [{ label: 'Escreve um rascunho de post sobre clareamento dental' }],\n permission: { feature: 'blog', action: 'create' as const },\n },\n ],\n // Host owns website nav; no admin nav in M1.\n navigation: [],\n routes: [\n { path: config.basePath, component: BlogListRoute, guard: 'public' },\n { path: `${config.basePath}/:slug`, component: PostDetailRoute, guard: 'public' },\n ],\n widgets: [],\n // Measuring what the posts brought in, and pushing a new one anywhere else.\n integrations: ['google-analytics', 'zapier'],\n // Owns the blog domain tables (categories + posts + anon-read view). The\n // marketing \"Blog\" backoffice writes to these; the website reads them.\n migrations: MIGRATIONS.map((m) => ({\n id: `blog-${m.id}`,\n version: MIGRATION_META[m.id]?.version ?? '1.0.0',\n sql: m.sql,\n description: MIGRATION_META[m.id]?.description ?? m.id,\n })),\n }\n\n return { manifest, Provider, dataProvider: provider }\n}\n\n// Public API\nexport type { BlogPluginOptions, ResolvedBlogConfig } from './config'\nexport type { BlogPost, BlogListQuery, BlogAuthor, BlogCategory } from './types'\nexport type { BlogDataProvider } from './data/types'\nexport { createMockBlogProvider } from './data/mock'\nexport { createSupabaseBlogProvider } from './data/supabase'\nexport type { BlogSeed, BlogSeedPost } from './data/mock'\nexport { BlogProvider, useBlogContext } from './context'\nexport type { BlogContextValue } from './context'\nexport { useBlogPosts } from './hooks/useBlogPosts'\nexport { usePost } from './hooks/usePost'\n\n// SSR seam: the reads as named loaders, resolvable before render, plus the\n// provider registry they read from.\nexport {\n registerBlogLoaders,\n BLOG_POSTS_LOADER,\n BLOG_POST_LOADER,\n} from './data/loaders'\nexport type { BlogPostsParams, BlogPostParams } from './data/loaders'\nexport { setBlogProvider, getBlogProvider, hasBlogProvider, resetBlogProvider } from './data/runtime'\n\n// The declared facts, exported so a connector can build against the contract\n// instead of hard-coding four strings.\nexport { buildBlogDomainEvents, BLOG_POST_EVENT_FIELDS } from './lib/blog-events'\n\n// SEO descriptors + schema.org builders.\nexport { postSeo, blogListSeo, articleJsonLd, blogJsonLd, postUrl, blogUrl } from './lib/seo'\nexport type { BlogSeoDescriptor, BlogSeoOptions } from './lib/seo'\nexport type { UseBlogPostsResult } from './hooks/useBlogPosts'\nexport type { UsePostResult } from './hooks/usePost'\n// The default components (BlogList, PostDetail, PostCard, AuthorAvatar) moved\n// to `@fayz-ai/plugin-blog/components` — they carry react-router-dom, and a\n// static re-export here would put it back into every consumer's bundle.\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fayz-ai/plugin-blog",
3
- "version": "0.12.25",
3
+ "version": "0.12.26",
4
4
  "description": "[experimental] Fayz SDK — blog / articles plugin (website surface)",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -27,7 +27,7 @@
27
27
  "lucide-react": ">=0.400.0 <1.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@fayz-ai/core": "^0.47.0"
30
+ "@fayz-ai/core": "^0.48.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^20.0.0",
@@ -1,5 +0,0 @@
1
- export type { BlogDataProvider } from './types';
2
- export { createMockBlogProvider } from './mock';
3
- export type { BlogSeed, BlogSeedPost, MockBlogProviderOptions } from './mock';
4
- export { createSupabaseBlogProvider } from './supabase';
5
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/data/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAA;AAC/C,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAA;AAC7E,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAA"}
@@ -1,5 +0,0 @@
1
- export { useBlogPosts } from './useBlogPosts';
2
- export type { UseBlogPostsResult } from './useBlogPosts';
3
- export { usePost } from './usePost';
4
- export type { UsePostResult } from './usePost';
5
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA"}