···1111import type { Attribute } from "src/replicache/attributes";
1212import { Database } from "supabase/database.types";
1313import * as Y from "yjs";
1414-import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
1414+import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
1515import { pool } from "supabase/pool";
16161717let supabase = createServerClient<Database>(
···1313import { PublishSmall } from "components/Icons/PublishSmall";
1414import { useIdentityData } from "components/IdentityProvider";
1515import { InputWithLabel } from "components/Input";
1616-import { Menu, MenuItem } from "components/Layout";
1616+import { Menu, MenuItem } from "components/Menu";
1717import {
1818 useLeafletDomains,
1919 useLeafletPublicationData,
···3535} from "src/hooks/queries/useBlocks";
3636import * as Y from "yjs";
3737import * as base64 from "base64-js";
3838-import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
3838+import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
3939import { BlueskyLogin } from "app/login/LoginForm";
4040import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication";
4141import { AddTiny } from "components/Icons/AddTiny";
4242+import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError";
42434344export const PublishButton = (props: { entityID: string }) => {
4445 let { data: pub } = useLeafletPublicationData();
···6869 let { identity } = useIdentityData();
6970 let toaster = useToaster();
70717272+ // Get title and description from Replicache state (same as draft editor)
7373+ // This ensures we use the latest edited values, not stale cached data
7474+ let replicacheTitle = useSubscribe(rep, (tx) =>
7575+ tx.get<string>("publication_title"),
7676+ );
7777+ let replicacheDescription = useSubscribe(rep, (tx) =>
7878+ tx.get<string>("publication_description"),
7979+ );
8080+8181+ // Use Replicache state if available, otherwise fall back to pub data
8282+ const currentTitle =
8383+ typeof replicacheTitle === "string" ? replicacheTitle : pub?.title || "";
8484+ const currentDescription =
8585+ typeof replicacheDescription === "string"
8686+ ? replicacheDescription
8787+ : pub?.description || "";
8888+7189 // Get tags from Replicache state (same as draft editor)
7290 let tags = useSubscribe(rep, (tx) => tx.get<string[]>("publication_tags"));
7391 const currentTags = Array.isArray(tags) ? tags : [];
9292+9393+ // Get cover image from Replicache state
9494+ let coverImage = useSubscribe(rep, (tx) =>
9595+ tx.get<string | null>("publication_cover_image"),
9696+ );
74977598 return (
7699 <ActionButton
···80103 onClick={async () => {
81104 if (!pub) return;
82105 setIsLoading(true);
8383- let doc = await publishToPublication({
106106+ let result = await publishToPublication({
84107 root_entity: rootEntity,
85108 publication_uri: pub.publications?.uri,
86109 leaflet_id: permission_token.id,
8787- title: pub.title,
8888- description: pub.description,
110110+ title: currentTitle,
111111+ description: currentDescription,
89112 tags: currentTags,
113113+ cover_image: coverImage,
90114 });
91115 setIsLoading(false);
92116 mutate();
93117118118+ if (!result.success) {
119119+ toaster({
120120+ content: isOAuthSessionError(result.error) ? (
121121+ <OAuthErrorMessage error={result.error} />
122122+ ) : (
123123+ "Failed to publish"
124124+ ),
125125+ type: "error",
126126+ });
127127+ return;
128128+ }
129129+94130 // Generate URL based on whether it's in a publication or standalone
95131 let docUrl = pub.publications
9696- ? `${getPublicationURL(pub.publications)}/${doc?.rkey}`
9797- : `https://leaflet.pub/p/${identity?.atp_did}/${doc?.rkey}`;
132132+ ? `${getPublicationURL(pub.publications)}/${result.rkey}`
133133+ : `https://leaflet.pub/p/${identity?.atp_did}/${result.rkey}`;
9813499135 toaster({
100136 content: (
101137 <div>
102138 {pub.doc ? "Updated! " : "Published! "}
103103- <SpeedyLink href={docUrl}>link</SpeedyLink>
139139+ <SpeedyLink className="underline" href={docUrl}>
140140+ See Published Post
141141+ </SpeedyLink>
104142 </div>
105143 ),
106144 type: "success",
+1-1
app/[leaflet_id]/actions/ShareOptions/index.tsx
···33import { getShareLink } from "./getShareLink";
44import { useEntitySetContext } from "components/EntitySetProvider";
55import { useSmoker } from "components/Toast";
66-import { Menu, MenuItem } from "components/Layout";
66+import { Menu, MenuItem } from "components/Menu";
77import { ActionButton } from "components/ActionBar/ActionButton";
88import useSWR from "swr";
99import LoginForm from "app/login/LoginForm";
+1-1
app/[leaflet_id]/page.tsx
···4455import type { Fact } from "src/replicache";
66import type { Attribute } from "src/replicache/attributes";
77-import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
77+import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
88import { Leaflet } from "./Leaflet";
99import { scanIndexLocal } from "src/replicache/utils";
1010import { getRSVPData } from "actions/getRSVPData";
+58-22
app/[leaflet_id]/publish/PublishPost.tsx
···2222import { TagSelector } from "../../../components/Tags";
2323import { LooseLeafSmall } from "components/Icons/LooseleafSmall";
2424import { PubIcon } from "components/ActionBar/Publications";
2525+import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError";
25262627type Props = {
2728 title: string;
···6566 let [charCount, setCharCount] = useState(0);
6667 let [shareOption, setShareOption] = useState<"bluesky" | "quiet">("bluesky");
6768 let [isLoading, setIsLoading] = useState(false);
6969+ let [oauthError, setOauthError] = useState<
7070+ import("src/atproto-oauth").OAuthSessionError | null
7171+ >(null);
6872 let params = useParams();
6973 let { rep } = useReplicache();
7074···7377 tx.get<string[]>("publication_tags"),
7478 );
7579 let [localTags, setLocalTags] = useState<string[]>([]);
8080+8181+ // Get cover image from Replicache
8282+ let replicacheCoverImage = useSubscribe(rep, (tx) =>
8383+ tx.get<string | null>("publication_cover_image"),
8484+ );
76857786 // Use Replicache tags only when we have a draft
7887 const hasDraft = props.hasDraft;
···96105 async function submit() {
97106 if (isLoading) return;
98107 setIsLoading(true);
108108+ setOauthError(null);
99109 await rep?.push();
100100- let doc = await publishToPublication({
110110+ let result = await publishToPublication({
101111 root_entity: props.root_entity,
102112 publication_uri: props.publication_uri,
103113 leaflet_id: props.leaflet_id,
104114 title: props.title,
105115 description: props.description,
106116 tags: currentTags,
117117+ cover_image: replicacheCoverImage,
107118 entitiesToDelete: props.entitiesToDelete,
108119 });
109109- if (!doc) return;
120120+121121+ if (!result.success) {
122122+ setIsLoading(false);
123123+ if (isOAuthSessionError(result.error)) {
124124+ setOauthError(result.error);
125125+ }
126126+ return;
127127+ }
110128111129 // Generate post URL based on whether it's in a publication or standalone
112130 let post_url = props.record?.base_path
113113- ? `https://${props.record.base_path}/${doc.rkey}`
114114- : `https://leaflet.pub/p/${props.profile.did}/${doc.rkey}`;
131131+ ? `https://${props.record.base_path}/${result.rkey}`
132132+ : `https://leaflet.pub/p/${props.profile.did}/${result.rkey}`;
115133116134 let [text, facets] = editorStateRef.current
117135 ? editorStateToFacetedText(editorStateRef.current)
118136 : [];
119119- if (shareOption === "bluesky")
120120- await publishPostToBsky({
137137+ if (shareOption === "bluesky") {
138138+ let bskyResult = await publishPostToBsky({
121139 facets: facets || [],
122140 text: text || "",
123141 title: props.title,
124142 url: post_url,
125143 description: props.description,
126126- document_record: doc.record,
127127- rkey: doc.rkey,
144144+ document_record: result.record,
145145+ rkey: result.rkey,
128146 });
147147+ if (!bskyResult.success && isOAuthSessionError(bskyResult.error)) {
148148+ setIsLoading(false);
149149+ setOauthError(bskyResult.error);
150150+ return;
151151+ }
152152+ }
129153 setIsLoading(false);
130154 props.setPublishState({ state: "success", post_url });
131155 }
···162186 </div>
163187 <hr className="border-border mb-2" />
164188165165- <div className="flex justify-between">
166166- <Link
167167- className="hover:no-underline! font-bold"
168168- href={`/${params.leaflet_id}`}
169169- >
170170- Back
171171- </Link>
172172- <ButtonPrimary
173173- type="submit"
174174- className="place-self-end h-[30px]"
175175- disabled={charCount > 300}
176176- >
177177- {isLoading ? <DotLoader /> : "Publish this Post!"}
178178- </ButtonPrimary>
189189+ <div className="flex flex-col gap-2">
190190+ <div className="flex justify-between">
191191+ <Link
192192+ className="hover:no-underline! font-bold"
193193+ href={`/${params.leaflet_id}`}
194194+ >
195195+ Back
196196+ </Link>
197197+ <ButtonPrimary
198198+ type="submit"
199199+ className="place-self-end h-[30px]"
200200+ disabled={charCount > 300}
201201+ >
202202+ {isLoading ? (
203203+ <DotLoader className="h-[23px]" />
204204+ ) : (
205205+ "Publish this Post!"
206206+ )}
207207+ </ButtonPrimary>
208208+ </div>
209209+ {oauthError && (
210210+ <OAuthErrorMessage
211211+ error={oauthError}
212212+ className="text-right text-sm text-accent-contrast"
213213+ />
214214+ )}
179215 </div>
180216 </div>
181217 </form>
+56-16
app/[leaflet_id]/publish/publishBskyPost.ts
···99import { TID } from "@atproto/common";
1010import { getIdentityData } from "actions/getIdentityData";
1111import { AtpBaseClient, PubLeafletDocument } from "lexicons/api";
1212-import { createOauthClient } from "src/atproto-oauth";
1212+import {
1313+ restoreOAuthSession,
1414+ OAuthSessionError,
1515+} from "src/atproto-oauth";
1316import { supabaseServerClient } from "supabase/serverClient";
1417import { Json } from "supabase/database.types";
1518import {
1619 getMicroLinkOgImage,
1720 getWebpageImage,
1821} from "src/utils/getMicroLinkOgImage";
2222+import { fetchAtprotoBlob } from "app/api/atproto_images/route";
2323+2424+type PublishBskyResult =
2525+ | { success: true }
2626+ | { success: false; error: OAuthSessionError };
19272028export async function publishPostToBsky(args: {
2129 text: string;
···2533 document_record: PubLeafletDocument.Record;
2634 rkey: string;
2735 facets: AppBskyRichtextFacet.Main[];
2828-}) {
2929- const oauthClient = await createOauthClient();
3636+}): Promise<PublishBskyResult> {
3037 let identity = await getIdentityData();
3131- if (!identity || !identity.atp_did) return null;
3838+ if (!identity || !identity.atp_did) {
3939+ return {
4040+ success: false,
4141+ error: {
4242+ type: "oauth_session_expired",
4343+ message: "Not authenticated",
4444+ did: "",
4545+ },
4646+ };
4747+ }
32483333- let credentialSession = await oauthClient.restore(identity.atp_did);
4949+ const sessionResult = await restoreOAuthSession(identity.atp_did);
5050+ if (!sessionResult.ok) {
5151+ return { success: false, error: sessionResult.error };
5252+ }
5353+ let credentialSession = sessionResult.value;
3454 let agent = new AtpBaseClient(
3555 credentialSession.fetchHandler.bind(credentialSession),
3656 );
3737- let newPostUrl = args.url;
3838- let preview_image = await getWebpageImage(newPostUrl, {
3939- width: 1400,
4040- height: 733,
4141- noCache: true,
4242- });
43574444- let binary = await preview_image.blob();
4545- let resized_preview_image = await sharp(await binary.arrayBuffer())
5858+ // Get image binary - prefer cover image, fall back to screenshot
5959+ let imageBinary: Blob | null = null;
6060+6161+ if (args.document_record.coverImage) {
6262+ let cid =
6363+ (args.document_record.coverImage.ref as unknown as { $link: string })[
6464+ "$link"
6565+ ] || args.document_record.coverImage.ref.toString();
6666+6767+ let coverImageResponse = await fetchAtprotoBlob(identity.atp_did, cid);
6868+ if (coverImageResponse) {
6969+ imageBinary = await coverImageResponse.blob();
7070+ }
7171+ }
7272+7373+ // Fall back to screenshot if no cover image or fetch failed
7474+ if (!imageBinary) {
7575+ let preview_image = await getWebpageImage(args.url, {
7676+ width: 1400,
7777+ height: 733,
7878+ noCache: true,
7979+ });
8080+ imageBinary = await preview_image.blob();
8181+ }
8282+8383+ // Resize and upload
8484+ let resizedImage = await sharp(await imageBinary.arrayBuffer())
4685 .resize({
4786 width: 1200,
8787+ height: 630,
4888 fit: "cover",
4989 })
5090 .webp({ quality: 85 })
5191 .toBuffer();
52925353- let blob = await agent.com.atproto.repo.uploadBlob(resized_preview_image, {
5454- headers: { "Content-Type": binary.type },
9393+ let blob = await agent.com.atproto.repo.uploadBlob(resizedImage, {
9494+ headers: { "Content-Type": "image/webp" },
5595 });
5696 let bsky = new BskyAgent(credentialSession);
5797 let post = await bsky.app.bsky.feed.post.create(
···90130 data: record as Json,
91131 })
92132 .eq("uri", result.uri);
9393- return true;
133133+ return { success: true };
94134}
+29-11
app/api/atproto_images/route.ts
···11import { IdResolver } from "@atproto/identity";
22import { NextRequest, NextResponse } from "next/server";
33+34let idResolver = new IdResolver();
4555-export async function GET(req: NextRequest) {
66- const url = new URL(req.url);
77- const params = {
88- did: url.searchParams.get("did") ?? "",
99- cid: url.searchParams.get("cid") ?? "",
1010- };
1111- if (!params.did || !params.cid)
1212- return new NextResponse(null, { status: 404 });
66+/**
77+ * Fetches a blob from an AT Protocol PDS given a DID and CID
88+ * Returns the Response object or null if the blob couldn't be fetched
99+ */
1010+export async function fetchAtprotoBlob(
1111+ did: string,
1212+ cid: string,
1313+): Promise<Response | null> {
1414+ if (!did || !cid) return null;
13151414- let identity = await idResolver.did.resolve(params.did);
1616+ let identity = await idResolver.did.resolve(did);
1517 let service = identity?.service?.find((f) => f.id === "#atproto_pds");
1616- if (!service) return new NextResponse(null, { status: 404 });
1818+ if (!service) return null;
1919+1720 const response = await fetch(
1818- `${service.serviceEndpoint}/xrpc/com.atproto.sync.getBlob?did=${params.did}&cid=${params.cid}`,
2121+ `${service.serviceEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`,
1922 {
2023 headers: {
2124 "Accept-Encoding": "gzip, deflate, br, zstd",
2225 },
2326 },
2427 );
2828+2929+ if (!response.ok) return null;
3030+3131+ return response;
3232+}
3333+3434+export async function GET(req: NextRequest) {
3535+ const url = new URL(req.url);
3636+ const params = {
3737+ did: url.searchParams.get("did") ?? "",
3838+ cid: url.searchParams.get("cid") ?? "",
3939+ };
4040+4141+ const response = await fetchAtprotoBlob(params.did, params.cid);
4242+ if (!response) return new NextResponse(null, { status: 404 });
25432644 // Clone the response to modify headers
2745 const cachedResponse = new Response(response.body, response);
+41
app/api/bsky/agent.ts
···11+import { Agent } from "@atproto/api";
22+import { cookies } from "next/headers";
33+import { createOauthClient } from "src/atproto-oauth";
44+import { supabaseServerClient } from "supabase/serverClient";
55+66+export async function getAuthenticatedAgent(): Promise<Agent | null> {
77+ try {
88+ const cookieStore = await cookies();
99+ const authToken =
1010+ cookieStore.get("auth_token")?.value ||
1111+ cookieStore.get("external_auth_token")?.value;
1212+1313+ if (!authToken || authToken === "null") return null;
1414+1515+ const { data } = await supabaseServerClient
1616+ .from("email_auth_tokens")
1717+ .select("identities(atp_did)")
1818+ .eq("id", authToken)
1919+ .eq("confirmed", true)
2020+ .single();
2121+2222+ const did = data?.identities?.atp_did;
2323+ if (!did) return null;
2424+2525+ const oauthClient = await createOauthClient();
2626+ const session = await oauthClient.restore(did);
2727+ return new Agent(session);
2828+ } catch (error) {
2929+ console.error("Failed to get authenticated agent:", error);
3030+ return null;
3131+ }
3232+}
3333+3434+export async function getAgent(): Promise<Agent> {
3535+ const agent = await getAuthenticatedAgent();
3636+ if (agent) return agent;
3737+3838+ return new Agent({
3939+ service: "https://public.api.bsky.app",
4040+ });
4141+}
···11+import { parse, ColorSpace, sRGB, distance, OKLab } from "colorjs.io/fn";
22+33+// define the color defaults for everything
44+export const ThemeDefaults = {
55+ "theme/page-background": "#FDFCFA",
66+ "theme/card-background": "#FFFFFF",
77+ "theme/primary": "#272727",
88+ "theme/highlight-1": "#FFFFFF",
99+ "theme/highlight-2": "#EDD280",
1010+ "theme/highlight-3": "#FFCDC3",
1111+1212+ //everywhere else, accent-background = accent-1 and accent-text = accent-2.
1313+ // we just need to create a migration pipeline before we can change this
1414+ "theme/accent-text": "#FFFFFF",
1515+ "theme/accent-background": "#0000FF",
1616+ "theme/accent-contrast": "#0000FF",
1717+};
1818+1919+// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast
2020+export function getColorDifference(color1: string, color2: string) {
2121+ ColorSpace.register(sRGB);
2222+ ColorSpace.register(OKLab);
2323+2424+ let parsedColor1 = parse(`rgb(${color1})`);
2525+ let parsedColor2 = parse(`rgb(${color2})`);
2626+2727+ return distance(parsedColor1, parsedColor2, "oklab");
2828+}
+1-1
components/ThemeManager/useColorAttribute.ts
···22import { Color, parseColor } from "react-aria-components";
33import { useEntity, useReplicache } from "src/replicache";
44import { FilterAttributes } from "src/replicache/attributes";
55-import { ThemeDefaults } from "./ThemeProvider";
55+import { ThemeDefaults } from "./themeUtils";
6677export function useColorAttribute(
88 entity: string | null,
···88import { useEntity, useReplicache } from "src/replicache";
99import * as Y from "yjs";
1010import * as base64 from "base64-js";
1111-import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
1111+import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
1212import { useParams, useRouter, useSearchParams } from "next/navigation";
1313import { focusBlock } from "src/utils/focusBlock";
1414import { useIsMobile } from "src/hooks/isMobile";
···11+-- Function to create homepage infrastructure for new identities
22+-- Replicates the logic from createIdentity TypeScript function
33+-- Returns the permission token ID to be used as home_page
44+CREATE OR REPLACE FUNCTION create_identity_homepage()
55+RETURNS uuid AS $$
66+DECLARE
77+ new_entity_set_id uuid;
88+ new_entity_id uuid;
99+ new_permission_token_id uuid;
1010+BEGIN
1111+ -- Create a new entity set
1212+ INSERT INTO entity_sets DEFAULT VALUES
1313+ RETURNING id INTO new_entity_set_id;
1414+1515+ -- Create a root entity and add it to that entity set
1616+ new_entity_id := gen_random_uuid();
1717+ INSERT INTO entities (id, set)
1818+ VALUES (new_entity_id, new_entity_set_id);
1919+2020+ -- Create a new permission token
2121+ INSERT INTO permission_tokens (root_entity)
2222+ VALUES (new_entity_id)
2323+ RETURNING id INTO new_permission_token_id;
2424+2525+ -- Give the token full permissions on that entity set
2626+ INSERT INTO permission_token_rights (token, entity_set, read, write, create_token, change_entity_set)
2727+ VALUES (new_permission_token_id, new_entity_set_id, true, true, true, true);
2828+2929+ RETURN new_permission_token_id;
3030+END;
3131+$$ LANGUAGE plpgsql;
3232+3333+-- Set the function as the default value for home_page column
3434+ALTER TABLE identities ALTER COLUMN home_page SET DEFAULT create_identity_homepage();
···11+-- site_standard_publications table (modeled off publications)
22+create table "public"."site_standard_publications" (
33+ "uri" text not null,
44+ "data" jsonb not null,
55+ "indexed_at" timestamp with time zone not null default now(),
66+ "identity_did" text not null
77+);
88+alter table "public"."site_standard_publications" enable row level security;
99+1010+-- site_standard_documents table (modeled off documents)
1111+create table "public"."site_standard_documents" (
1212+ "uri" text not null,
1313+ "data" jsonb not null,
1414+ "indexed_at" timestamp with time zone not null default now(),
1515+ "identity_did" text not null
1616+);
1717+alter table "public"."site_standard_documents" enable row level security;
1818+1919+-- site_standard_documents_in_publications relation table (modeled off documents_in_publications)
2020+create table "public"."site_standard_documents_in_publications" (
2121+ "publication" text not null,
2222+ "document" text not null,
2323+ "indexed_at" timestamp with time zone not null default now()
2424+);
2525+alter table "public"."site_standard_documents_in_publications" enable row level security;
2626+2727+-- Primary key indexes
2828+CREATE UNIQUE INDEX site_standard_publications_pkey ON public.site_standard_publications USING btree (uri);
2929+CREATE UNIQUE INDEX site_standard_documents_pkey ON public.site_standard_documents USING btree (uri);
3030+CREATE UNIQUE INDEX site_standard_documents_in_publications_pkey ON public.site_standard_documents_in_publications USING btree (publication, document);
3131+3232+-- Add primary key constraints
3333+alter table "public"."site_standard_publications" add constraint "site_standard_publications_pkey" PRIMARY KEY using index "site_standard_publications_pkey";
3434+alter table "public"."site_standard_documents" add constraint "site_standard_documents_pkey" PRIMARY KEY using index "site_standard_documents_pkey";
3535+alter table "public"."site_standard_documents_in_publications" add constraint "site_standard_documents_in_publications_pkey" PRIMARY KEY using index "site_standard_documents_in_publications_pkey";
3636+3737+-- Foreign key constraints for identity relations
3838+alter table "public"."site_standard_publications" add constraint "site_standard_publications_identity_did_fkey" FOREIGN KEY (identity_did) REFERENCES identities(atp_did) ON DELETE CASCADE not valid;
3939+alter table "public"."site_standard_publications" validate constraint "site_standard_publications_identity_did_fkey";
4040+alter table "public"."site_standard_documents" add constraint "site_standard_documents_identity_did_fkey" FOREIGN KEY (identity_did) REFERENCES identities(atp_did) ON DELETE CASCADE not valid;
4141+alter table "public"."site_standard_documents" validate constraint "site_standard_documents_identity_did_fkey";
4242+4343+-- Foreign key constraints for relation table
4444+alter table "public"."site_standard_documents_in_publications" add constraint "site_standard_documents_in_publications_document_fkey" FOREIGN KEY (document) REFERENCES site_standard_documents(uri) ON DELETE CASCADE not valid;
4545+alter table "public"."site_standard_documents_in_publications" validate constraint "site_standard_documents_in_publications_document_fkey";
4646+alter table "public"."site_standard_documents_in_publications" add constraint "site_standard_documents_in_publications_publication_fkey" FOREIGN KEY (publication) REFERENCES site_standard_publications(uri) ON DELETE CASCADE not valid;
4747+alter table "public"."site_standard_documents_in_publications" validate constraint "site_standard_documents_in_publications_publication_fkey";
4848+4949+-- Grants for site_standard_publications
5050+grant delete on table "public"."site_standard_publications" to "anon";
5151+grant insert on table "public"."site_standard_publications" to "anon";
5252+grant references on table "public"."site_standard_publications" to "anon";
5353+grant select on table "public"."site_standard_publications" to "anon";
5454+grant trigger on table "public"."site_standard_publications" to "anon";
5555+grant truncate on table "public"."site_standard_publications" to "anon";
5656+grant update on table "public"."site_standard_publications" to "anon";
5757+grant delete on table "public"."site_standard_publications" to "authenticated";
5858+grant insert on table "public"."site_standard_publications" to "authenticated";
5959+grant references on table "public"."site_standard_publications" to "authenticated";
6060+grant select on table "public"."site_standard_publications" to "authenticated";
6161+grant trigger on table "public"."site_standard_publications" to "authenticated";
6262+grant truncate on table "public"."site_standard_publications" to "authenticated";
6363+grant update on table "public"."site_standard_publications" to "authenticated";
6464+grant delete on table "public"."site_standard_publications" to "service_role";
6565+grant insert on table "public"."site_standard_publications" to "service_role";
6666+grant references on table "public"."site_standard_publications" to "service_role";
6767+grant select on table "public"."site_standard_publications" to "service_role";
6868+grant trigger on table "public"."site_standard_publications" to "service_role";
6969+grant truncate on table "public"."site_standard_publications" to "service_role";
7070+grant update on table "public"."site_standard_publications" to "service_role";
7171+7272+-- Grants for site_standard_documents
7373+grant delete on table "public"."site_standard_documents" to "anon";
7474+grant insert on table "public"."site_standard_documents" to "anon";
7575+grant references on table "public"."site_standard_documents" to "anon";
7676+grant select on table "public"."site_standard_documents" to "anon";
7777+grant trigger on table "public"."site_standard_documents" to "anon";
7878+grant truncate on table "public"."site_standard_documents" to "anon";
7979+grant update on table "public"."site_standard_documents" to "anon";
8080+grant delete on table "public"."site_standard_documents" to "authenticated";
8181+grant insert on table "public"."site_standard_documents" to "authenticated";
8282+grant references on table "public"."site_standard_documents" to "authenticated";
8383+grant select on table "public"."site_standard_documents" to "authenticated";
8484+grant trigger on table "public"."site_standard_documents" to "authenticated";
8585+grant truncate on table "public"."site_standard_documents" to "authenticated";
8686+grant update on table "public"."site_standard_documents" to "authenticated";
8787+grant delete on table "public"."site_standard_documents" to "service_role";
8888+grant insert on table "public"."site_standard_documents" to "service_role";
8989+grant references on table "public"."site_standard_documents" to "service_role";
9090+grant select on table "public"."site_standard_documents" to "service_role";
9191+grant trigger on table "public"."site_standard_documents" to "service_role";
9292+grant truncate on table "public"."site_standard_documents" to "service_role";
9393+grant update on table "public"."site_standard_documents" to "service_role";
9494+9595+-- Grants for site_standard_documents_in_publications
9696+grant delete on table "public"."site_standard_documents_in_publications" to "anon";
9797+grant insert on table "public"."site_standard_documents_in_publications" to "anon";
9898+grant references on table "public"."site_standard_documents_in_publications" to "anon";
9999+grant select on table "public"."site_standard_documents_in_publications" to "anon";
100100+grant trigger on table "public"."site_standard_documents_in_publications" to "anon";
101101+grant truncate on table "public"."site_standard_documents_in_publications" to "anon";
102102+grant update on table "public"."site_standard_documents_in_publications" to "anon";
103103+grant delete on table "public"."site_standard_documents_in_publications" to "authenticated";
104104+grant insert on table "public"."site_standard_documents_in_publications" to "authenticated";
105105+grant references on table "public"."site_standard_documents_in_publications" to "authenticated";
106106+grant select on table "public"."site_standard_documents_in_publications" to "authenticated";
107107+grant trigger on table "public"."site_standard_documents_in_publications" to "authenticated";
108108+grant truncate on table "public"."site_standard_documents_in_publications" to "authenticated";
109109+grant update on table "public"."site_standard_documents_in_publications" to "authenticated";
110110+grant delete on table "public"."site_standard_documents_in_publications" to "service_role";
111111+grant insert on table "public"."site_standard_documents_in_publications" to "service_role";
112112+grant references on table "public"."site_standard_documents_in_publications" to "service_role";
113113+grant select on table "public"."site_standard_documents_in_publications" to "service_role";
114114+grant trigger on table "public"."site_standard_documents_in_publications" to "service_role";
115115+grant truncate on table "public"."site_standard_documents_in_publications" to "service_role";
116116+grant update on table "public"."site_standard_documents_in_publications" to "service_role";
117117+118118+-- site_standard_subscriptions table (modeled off publication_subscriptions)
119119+create table "public"."site_standard_subscriptions" (
120120+ "publication" text not null,
121121+ "identity" text not null,
122122+ "created_at" timestamp with time zone not null default now(),
123123+ "record" jsonb not null,
124124+ "uri" text not null
125125+);
126126+alter table "public"."site_standard_subscriptions" enable row level security;
127127+128128+-- Primary key and unique indexes
129129+CREATE UNIQUE INDEX site_standard_subscriptions_pkey ON public.site_standard_subscriptions USING btree (publication, identity);
130130+CREATE UNIQUE INDEX site_standard_subscriptions_uri_key ON public.site_standard_subscriptions USING btree (uri);
131131+132132+-- Add constraints
133133+alter table "public"."site_standard_subscriptions" add constraint "site_standard_subscriptions_pkey" PRIMARY KEY using index "site_standard_subscriptions_pkey";
134134+alter table "public"."site_standard_subscriptions" add constraint "site_standard_subscriptions_uri_key" UNIQUE using index "site_standard_subscriptions_uri_key";
135135+alter table "public"."site_standard_subscriptions" add constraint "site_standard_subscriptions_publication_fkey" FOREIGN KEY (publication) REFERENCES site_standard_publications(uri) ON DELETE CASCADE not valid;
136136+alter table "public"."site_standard_subscriptions" validate constraint "site_standard_subscriptions_publication_fkey";
137137+alter table "public"."site_standard_subscriptions" add constraint "site_standard_subscriptions_identity_fkey" FOREIGN KEY (identity) REFERENCES identities(atp_did) ON DELETE CASCADE not valid;
138138+alter table "public"."site_standard_subscriptions" validate constraint "site_standard_subscriptions_identity_fkey";
139139+140140+-- Grants for site_standard_subscriptions
141141+grant delete on table "public"."site_standard_subscriptions" to "anon";
142142+grant insert on table "public"."site_standard_subscriptions" to "anon";
143143+grant references on table "public"."site_standard_subscriptions" to "anon";
144144+grant select on table "public"."site_standard_subscriptions" to "anon";
145145+grant trigger on table "public"."site_standard_subscriptions" to "anon";
146146+grant truncate on table "public"."site_standard_subscriptions" to "anon";
147147+grant update on table "public"."site_standard_subscriptions" to "anon";
148148+grant delete on table "public"."site_standard_subscriptions" to "authenticated";
149149+grant insert on table "public"."site_standard_subscriptions" to "authenticated";
150150+grant references on table "public"."site_standard_subscriptions" to "authenticated";
151151+grant select on table "public"."site_standard_subscriptions" to "authenticated";
152152+grant trigger on table "public"."site_standard_subscriptions" to "authenticated";
153153+grant truncate on table "public"."site_standard_subscriptions" to "authenticated";
154154+grant update on table "public"."site_standard_subscriptions" to "authenticated";
155155+grant delete on table "public"."site_standard_subscriptions" to "service_role";
156156+grant insert on table "public"."site_standard_subscriptions" to "service_role";
157157+grant references on table "public"."site_standard_subscriptions" to "service_role";
158158+grant select on table "public"."site_standard_subscriptions" to "service_role";
159159+grant trigger on table "public"."site_standard_subscriptions" to "service_role";
160160+grant truncate on table "public"."site_standard_subscriptions" to "service_role";
161161+grant update on table "public"."site_standard_subscriptions" to "service_role";