From 8dae14d3e59a295e5da7f5379e60fc1aa0250ba4 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Mon, 11 Mar 2024 21:47:36 -0700 Subject: [PATCH 1/8] feat: update sql faqs --- docs/sql/faqs.md | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/sql/faqs.md b/docs/sql/faqs.md index 4919165..ca93d07 100644 --- a/docs/sql/faqs.md +++ b/docs/sql/faqs.md @@ -62,15 +62,21 @@ It doesn't matter if you use all caps or all lowercase—either way is perfectly The following is a list of cool and useful SQL-related tools. These are not affiliated with Tableland but simply being shared to help developers using the protocol: -- [https://www.beekeeperstudio.io/](https://www.beekeeperstudio.io/) -- [https://prql-lang.org/](https://prql-lang.org/) -- [https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/](https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/) -- [https://dataland.io/](https://dataland.io/) -- [https://github.com/datafold/data-diff](https://github.com/datafold/data-diff) -- [https://www.dolthub.com/](https://www.dolthub.com/) -- [https://principles.planetscale.com/](https://principles.planetscale.com/) -- [https://sqliteviewer.app/](https://sqliteviewer.app/) -- [https://codebeautify.org/sqlformatter](https://codebeautify.org/sqlformatter) -- [https://beta.openai.com/examples/default-sql-request](https://beta.openai.com/examples/default-sql-request) -- Mock SQL data ⇒ [https://www.mockaroo.com/](https://www.mockaroo.com/) -- [https://softwium.com/mockium/](https://softwium.com/mockium/) +- Online SQL viewer/editor: + - [https://www.beekeeperstudio.io/](https://www.beekeeperstudio.io/) + - [https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/](https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/) + - [https://dataland.io/](https://dataland.io/) + - [https://sqliteviewer.app/](https://sqliteviewer.app/) +- SQL formatter: + - [https://sqltools.beekeeperstudio.io/format](https://sqltools.beekeeperstudio.io/format) + - [https://codebeautify.org/sqlformatter](https://codebeautify.org/sqlformatter) +- Mock SQL data: + - [https://www.mockaroo.com/](https://www.mockaroo.com/) + - [https://softwium.com/mockium/](https://softwium.com/mockium/) +- SQL replacement pipeline: [https://prql-lang.org/](https://prql-lang.org/) +- Data migration & diffs: + - [https://github.com/datafold/data-diff](https://github.com/datafold/data-diff) + - [https://www.dolthub.com/](https://www.dolthub.com/) +- Natural language to SQL: [https://platform.openai.com/examples/default-sql-translate](https://platform.openai.com/examples/default-sql-translate) + +- SQL principles by PlanetScale: [https://principles.planetscale.com/](https://principles.planetscale.com/) From be593f96e0196b2ee48af6ee3f6b57edf987c5cd Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Mon, 11 Mar 2024 22:05:58 -0700 Subject: [PATCH 2/8] feat: update showcase component & data --- src/components/projects.ts | 12 +++++++++++- src/pages/showcase/index.tsx | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/components/projects.ts b/src/components/projects.ts index 2f242c6..f986814 100644 --- a/src/components/projects.ts +++ b/src/components/projects.ts @@ -14,7 +14,7 @@ export default [ website: "https://dimo.zone", github: "https://github.com/tablelandnetwork/dimo-vehicle-defs", twitter: "https://twitter.com/DIMO_Network", - details: `DIMO is an open and user-owned network. DIMO is an open and user-owned network where drivers can plug DIMO hardware into their cars and earn rewards for the data they share.\n\nPart of DIMO's solution is a vehicle definitions registry that stores vehicle data in a standardized format. With Tableland, this table is publicly queryable and can be used by anyone to build applications on top of the data.\n\nFrom a design perspective, the table is owned by and written to by a VehicleId NFT contract, and VehicleId tokens represent user vehicles and are associated with a vehicle definition (e.g., 2011 Toyota Tacoma). The crux of the problem is how to ensure that a given vehicle definition exists when a new VehicleId is minted from the contract.`, + details: `DIMO is an open and user-owned network where drivers can plug DIMO hardware into their cars and earn rewards for the data they share.\n\nPart of DIMO's solution is a vehicle definitions registry that stores vehicle data in a standardized format. With Tableland, this table is publicly queryable and can be used by anyone to build applications on top of the data.\n\nFrom a design perspective, the table is owned by and written to by a VehicleId NFT contract, and VehicleId tokens represent user vehicles and are associated with a vehicle definition (e.g., 2011 Toyota Tacoma). The crux of the problem is how to ensure that a given vehicle definition exists when a new VehicleId is minted from the contract.`, }, { name: "WeatherXM", @@ -28,6 +28,16 @@ export default [ github: "https://github.com/WeatherXM", twitter: "https://twitter.com/WeatherXM", details: `WeatherXM is a network that's powered by community owned devices, purpose built by WeatherXM for weather data collection. As part of the network, devices are incentivized to share data, which is sent to various stations throughout the globe.\n\nOne of the challenges with this approach is not only ensuring that the raw device data is available when it's needed, but as a decentralized network, its infrastructure must also make sure that data is stored in a decentralized fashion instead of centralized infrastructure.\n\nAs devices send data to the network, it's replicated to a hot for immediate and TTL retrieval, which is pertinent for compute over that data. And for persistence guarantees, this data is also replicated to Filecoin for long-term storage guarantees, which can help the network ensure that data is available for years to come.`, + blogs: [ + { + name: "WeatherXM + Textile: DePIN Data Storage, Retrieval, & Rewards Computation", + url: "https://mirror.xyz/tableland.eth/iw5tqM_2HFaTdTnQeZRw_fuj0UwlvxMaJOL2pJ3hIrQ", + }, + { + name: "Solutions for DePIN Data Management & Collaboration", + url: "https://mirror.xyz/tableland.eth/CbRLSRYYClTB8bNOph-1z9WkFvHzBs5MIImPu-ybwAs", + }, + ], }, { name: "Hideout Labs", diff --git a/src/pages/showcase/index.tsx b/src/pages/showcase/index.tsx index f7443e1..e1d30d1 100644 --- a/src/pages/showcase/index.tsx +++ b/src/pages/showcase/index.tsx @@ -13,6 +13,11 @@ import YoutubeEmbed from "@site/src/components/YoutubeEmbed"; import projectsData from "../../components/projects"; import styles from "./index.module.css"; +interface Blog { + name: string; + url: string; +} + export interface Project { name: string; // Name of the project description: string; // Short description @@ -25,6 +30,7 @@ export interface Project { twitter: string; // Twitter link details: string; // Long form details about the project youtubeId?: string; // Youtube demo ID, e.g., `-MUq--Nrd0c` for `https://www.youtube.com/watch?v=-MUq--Nrd0c` + blogs?: Blog[]; // Blog links } export type Protocol = "Tableland" | "Textile"; @@ -340,6 +346,19 @@ export default function Showcase() {

{currentProject.details}

+ {currentProject.blogs && + currentProject.blogs?.length > 0 && ( + <> +

Blog posts

+ + + )} {currentProject.youtubeId && ( <>

Demo

From 2d0a9d5c002bb6c60d596ab4933d4e23ad04783f Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 12 Mar 2024 14:00:18 -0700 Subject: [PATCH 3/8] feat: update wagmi example for v2 --- docs/playbooks/frameworks/wagmi.md | 167 +++++++++++++++++++++++++---- 1 file changed, 147 insertions(+), 20 deletions(-) diff --git a/docs/playbooks/frameworks/wagmi.md b/docs/playbooks/frameworks/wagmi.md index 5b15b53..bec35cb 100644 --- a/docs/playbooks/frameworks/wagmi.md +++ b/docs/playbooks/frameworks/wagmi.md @@ -15,13 +15,26 @@ One great library to use in React apps is [wagmi](https://wagmi.sh/). It offers ## 1. Installation -First, install wagmi, RainbowKit, Tableland, and ethers. If you need help setting up a React project, check out the [React framework quickstart](reactjs). +First, install wagmi, RainbowKit, Tableland, and ethers. If you need help setting up a React project, check out the [React framework quickstart](reactjs). All of these examples will show how to use either wagmi v1 or v2, so you can choose which version you'd like to use. + + + + +```bash npm2yarn +npm install wagmi@latest @rainbow-me/rainbowkit@latest @tableland/sdk ethers@^5.7.2 @tanstack/react-query +``` + + + ```bash npm2yarn -npm install --save wagmi @rainbow-me/rainbowkit @tableland/sdk ethers@^5.7.2 +npm install wagmi@^1 @rainbow-me/rainbowkit@^1 @tableland/sdk ethers@^5.7.2 ``` -Note that RainbowKit uses WalletConnect under the hood, which requires you to sign up for an account—[see their docs](https://www.rainbowkit.com/guides/walletconnect-v2) for more details. These examples will use [Alchemy](https://www.alchemy.com/) as the RPC provider, but you can choose whatever provider you'd like. + + + +Note that RainbowKit uses WalletConnect under the hood, which requires you to sign up for an account—[see their docs](https://www.rainbowkit.com/guides/walletconnect-v2) for more details. Now, let's set up our React app with a Vite scaffold: @@ -33,14 +46,55 @@ npm create vite@latest starter-app -- --template react ### Wagmi -We'll use a few different files to properly set everything up. Let's start with the first—create a file called `wagmi.js` to handle chain configurations. You should first create a `.env` that handles things like API keys. Our example with use Alchemy for the provider URL and also use WalletConnect's API for the connector. The file should contain: +We'll use a few different files to properly set everything up. Let's start with the first—create a file called `wagmi.js` to handle chain configurations. You should first create a `.env` that handles things like API keys. Our example uses a public provider URL and will also use WalletConnect's API for the connector. The file should contain: ```txt title=".env" -VITE_ALCHEMY_API_KEY=your_key VITE_WALLET_CONNECT_PROJECT_ID=your_key +ENABLE_TESTNETS=true +``` + +Note that we'll use `import.meta.env` to access these variables (below) in the `wagmi.js` file. Depending on your frontend setup, it may differ. With Vite, you use `import.meta.env` and prefix the variable with `VITE_`. But, if you're using Next.js, you'd use `process.env` and prefix with `NEXT_PUBLIC_`. + + + + +```jsx title="src/wagmi.js" +import "@rainbow-me/rainbowkit/styles.css"; +import { getDefaultConfig } from "@rainbow-me/rainbowkit"; +import * as chain from "wagmi/chains"; +import { http } from "viem"; + +const chains = [ + chain.mainnet, + chain.polygon, + chain.optimism, + chain.arbitrum, + chain.arbitrumNova, + chain.filecoin, + ...(import.meta.ENABLE_TESTNETS === "true" + ? [ + chain.arbitrumSepolia, + chain.sepolia, + chain.polygonMumbai, + chain.optimismSepolia, + chain.filecoinCalibration, + chain.hardhat, + ] + : []), +]; + +const transports = Object.fromEntries(chains.map((c) => [c.id, http()])); + +export const config = getDefaultConfig({ + appName: "Tableland Starter", + chains, + transports, + projectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID ?? "", +}); ``` -Note that we'll use `import.meta.env` to access these variables (below) in the `wagmi.js` file. Depending on your frontend setup, it may differ. + + ```js title="src/wagmi.js" import "@rainbow-me/rainbowkit/styles.css"; @@ -50,30 +104,32 @@ import * as chain from "wagmi/chains"; import { publicProvider } from "wagmi/providers/public"; import { alchemyProvider } from "wagmi/providers/alchemy"; -// All of the chains configured below are supported by Tableland const { chains, publicClient, webSocketPublicClient } = configureChains( [ chain.mainnet, chain.polygon, chain.optimism, chain.arbitrum, - chain.sepolia, - chain.polygonMumbai, - chain.optimismSepolia, - chain.arbitrumSepolia, - chain.filecoinCalibration, - chain.hardhat, + chain.arbitrumNova, + chain.filecoin, + ...(import.meta.ENABLE_TESTNETS === "true" + ? [ + chain.arbitrumSepolia, + chain.sepolia, + chain.polygonMumbai, + chain.optimismSepolia, + chain.filecoinCalibration, + chain.hardhat, + ] + : []), ], - [ - alchemyProvider({ apiKey: VITE_ALCHEMY_API_KEY ?? "" }), // Set up an Alchemy account: https://www.alchemy.com/ - publicProvider(), - ] + [publicProvider()] ); const { connectors } = getDefaultWallets({ appName: "Tableland Starter", chains, - projectId: process.env.WALLET_CONNECT_PROJECT_ID ?? "", // Set up a WalletConnect account: https://walletconnect.com/ + projectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID ?? "", }); export const config = createConfig({ @@ -86,10 +142,43 @@ export const config = createConfig({ export { chains }; ``` + + + ### Providers Next, we'll set up providers that wrap our React component with the wagmi config. Create a file called `providers.jsx` and add the following: + + + +```jsx title="src/providers.jsx" +import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import * as React from "react"; +import { WagmiProvider } from "wagmi"; +import { config } from "./wagmi"; + +const queryClient = new QueryClient(); + +export function Providers({ children }) { + const [mounted, setMounted] = React.useState(false); + React.useEffect(() => setMounted(true), []); + return ( + + + + {mounted && children} + + + + ); +} +``` + + + + ```js title="src/providers.jsx" import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit"; import * as React from "react"; @@ -109,6 +198,9 @@ export function Providers({ children }) { } ``` + + + ### App component In your `main.jsx` component, replace it with the following: @@ -133,7 +225,39 @@ ReactDOM.createRoot(document.getElementById("root")).render( Since wagmi is not natively compatible with ethers, we'll need to create a hook that adapts the wagmi account (viem) to an ethers account. Create a directory called `hooks` and a file called `useSigner.js`. Then, add the following: -```js title="src/hooks/useEthersAccount.js" + + + +```js title="src/hooks/useSigner.js" +import { useMemo } from "react"; +import { providers } from "ethers"; +import { useWalletClient } from "wagmi"; + +function walletClientToSigner(walletClient) { + const { account, chain, transport } = walletClient; + const network = { + chainId: chain.id, + name: chain.name, + ensAddress: chain.contracts?.ensRegistry?.address, + }; + const provider = new providers.JsonRpcProvider(transport.url, network); + const signer = provider.getSigner(account.address); + return signer; +} + +export function useSigner({ chainId } = {}) { + const { data: walletClient } = useWalletClient({ chainId }); + return useMemo( + () => (walletClient ? walletClientToSigner(walletClient) : undefined), + [walletClient] + ); +} +``` + + + + +```js title="src/hooks/useSigner.js" // Convert wagmi/viem `WalletClient` to ethers `Signer` import { useMemo } from "react"; import { useWalletClient } from "wagmi"; @@ -160,6 +284,9 @@ export function useSigner({ chainId } = {}) { } ``` + + + ## 3. Tableland setup Now that we have everything ready, we can use the `useSigner` hook we just created in our Tableland setup. Create a file called `Tableland.jsx` and add the following. We'll keep this example simple and just show how to create a table, which uses the `useSigner` hook that's stored as the `signer` variable in the app's state. @@ -226,7 +353,7 @@ function App() { export default App; ``` -This boilerplate will display a connect wallet button in the navbar. Once a user makes the connection, the signer will now be available in the Tableland logic from step 3! +This boilerplate will display a connect wallet button in the navbar. Once a user makes the connection, the signer will now be available in the Tableland logic! :::tip For more examples, you can [check out the templates](/quickstarts/templates) we've created, which also include a TypeScript example of what we walked through as well as Next.js examples. From 9977b0eaa3566a0fd0cf8657a0c2608b6f2a0ec6 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 12 Mar 2024 14:13:13 -0700 Subject: [PATCH 4/8] feat: delete unused Wallet component --- docs/contribute/style-guide.md | 1 - src/components/Wallet/index.tsx | 58 ------------------------- src/components/Wallet/styles.module.css | 9 ---- 3 files changed, 68 deletions(-) delete mode 100644 src/components/Wallet/index.tsx delete mode 100644 src/components/Wallet/styles.module.css diff --git a/docs/contribute/style-guide.md b/docs/contribute/style-guide.md index c330525..c2020fe 100644 --- a/docs/contribute/style-guide.md +++ b/docs/contribute/style-guide.md @@ -194,7 +194,6 @@ console.log(name.toUpperCase()); // Uncaught TypeError: Cannot read properties of null (reading 'toUpperCase') ``` -import { Address } from '@site/src/components/Wallet' import { SupportedChains, ChainsList, ChainInfo } from '@site/src/components/SupportedChains' ## Components diff --git a/src/components/Wallet/index.tsx b/src/components/Wallet/index.tsx deleted file mode 100644 index 5eaf912..0000000 --- a/src/components/Wallet/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; -// TMP: disable wagmi due to build error: -// `ReferenceError: window is not defined` -// import { useAccount, useConnect, useDisconnect } from "wagmi"; -// import { InjectedConnector } from "wagmi/connectors/injected"; -import clsx from "clsx"; -import styles from "./styles.module.css"; -/* -export function getWalletAddress(): string | undefined { - const { address, isConnected } = useAccount(); - // Get the user's connected wallet address. - if (isConnected) { - return address; - } else return "0xINSERT_ADDRESS"; -} -*/ -export const Address = (): JSX.Element => { - // const address = getWalletAddress(); - const address = undefined; - // Return the user's connected wallet, or some default value (e.g., this is - // the second Hardhat wallet address). - return address ? <>{address} : <>0xINSERT_ADDRESS; -}; -/* -export default function ConnectWallet() { - const { address, isConnected } = useAccount(); - const { connect } = useConnect({ - connector: new InjectedConnector(), - }); - const { disconnect } = useDisconnect(); - - if (isConnected) - return ( - - ); - return ( - - ); -} -*/ diff --git a/src/components/Wallet/styles.module.css b/src/components/Wallet/styles.module.css deleted file mode 100644 index 1a9597d..0000000 --- a/src/components/Wallet/styles.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.profileBtn { - width: 6rem; -} - -@media (max-width: 996px) { - .profileBtn { - display: none; - } -} From ecfe3b964593703d9a45b05f35189b839701787a Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 12 Mar 2024 14:22:01 -0700 Subject: [PATCH 5/8] feat: 'onchain' messaging | add chain contract list --- docs/fundamentals/supported-chains.md | 6 ++++-- docusaurus.config.ts | 2 +- src/components/Home/index.tsx | 22 +++++++++------------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/fundamentals/supported-chains.md b/docs/fundamentals/supported-chains.md index 05f2c82..fe07fad 100644 --- a/docs/fundamentals/supported-chains.md +++ b/docs/fundamentals/supported-chains.md @@ -7,7 +7,7 @@ keywords: - blockchains --- -import { ChainsList } from '@site/src/components/SupportedChains' +import { ChainsList, SupportedChains } from '@site/src/components/SupportedChains' Chain selection makes a significant impact on the database's usability. Choose a chain that’s too slow or expensive, and it won’t be feasible for table writes to occur frequently. Deploy a cross-chain data model for value layering purposes, and certain onchain access control features are lost. This page explains general chain related concepts; the following chains are supported and described in more detail in the subsequent pages. @@ -22,7 +22,9 @@ Review the [cost estimation](/fundamentals/architecture/cost-estimator) table th ## Chain information -If would like to dive straight into chain-specific overviews, with decision considerations and other chain information (chain ID, contracts, block explorers, faucets, etc.), head to one of the pages below. The full list of chain-specific details can be found in the [chain info pages](/quickstarts/chains). +If would like to dive straight into chain-specific overviews, with decision considerations and other chain information (chain ID, contracts, block explorers, faucets, etc.), head to one of the pages below. The full list of chain-specific details can be found in the [chain info pages](/quickstarts/chains). The following shows the deployed contracts addresses across all mainnet and testnet chains: + + Here's a summary of how the chains works in terms of speed: diff --git a/docusaurus.config.ts b/docusaurus.config.ts index cb5a2e9..01a34e8 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -10,7 +10,7 @@ async function createConfig(): Promise { return { title: "Tableland Docs", tagline: - "Explore how to store & query data on Tableland—the serverless web3 database for apps.", + "Explore how to store & query data on Tableland—the onchain SQL database.", url: "https://docs.tableland.xyz", baseUrl: "/", onBrokenLinks: "log", // Or, could `throw` diff --git a/src/components/Home/index.tsx b/src/components/Home/index.tsx index 9727005..43d4f8c 100644 --- a/src/components/Home/index.tsx +++ b/src/components/Home/index.tsx @@ -20,17 +20,16 @@ export default function Home(): JSX.Element {
-

Program your web3 data

+

Program your onchain data

- Tableland is a decentralized database built on the SQLite - engine, providing developers with a web3-native, relational - database that easily integrates into their stack. With - Tableland, you can: + Tableland is a database built on the SQLite engine, providing + developers with a web3-native, relational database that easily + integrates into their stack. With Tableland, you can:

  • - Utilize SQL to interact with web3 data, making the development - process simpler and more efficient. + Utilize SQL to interact with onchain data, making the + development process simpler and more efficient.
  • Configure row-level access rules driven by wallet addresses, @@ -38,17 +37,14 @@ export default function Home(): JSX.Element {
  • Build robust data pipelines that process and distribute large - amounts of data for DeFi, DeSci, games, and more, all - decentralized and autonomous, leveraging Tableland's - infrastructure. + amounts of data for DePIN, DeSci, games, and more, all with + smart contracts or SDKs that leverage serverless infra.
{" "}

Deploy across{" "} - - multiple blockchains - + multiple chains , including Filecoin, Ethereum, Polygon, Arbitrum, and Optimism—and be a part of the growing number of projects using Tableland for data-driven applications. From d7db099b3127bd878bc6fb9303ede586709d0231 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Mon, 18 Mar 2024 17:16:37 -0700 Subject: [PATCH 6/8] feat: add Lit Protocol tutorial --- config/sidebars.ts | 1 + docs/playbooks/README.md | 1 + docs/playbooks/integrations/lit.md | 730 +++++++++++++++++++++++++++++ 3 files changed, 732 insertions(+) create mode 100644 docs/playbooks/integrations/lit.md diff --git a/config/sidebars.ts b/config/sidebars.ts index 52e5bd4..dee6c70 100644 --- a/config/sidebars.ts +++ b/config/sidebars.ts @@ -171,6 +171,7 @@ const playbooks = [ "playbooks/frameworks/wagmi", ...section("Protocol integrations"), "playbooks/integrations/ipfs", + "playbooks/integrations/lit", // ...section("Platforms"), // "playbooks/platforms/spheron", ]; diff --git a/docs/playbooks/README.md b/docs/playbooks/README.md index db7d14a..ff5995c 100644 --- a/docs/playbooks/README.md +++ b/docs/playbooks/README.md @@ -47,3 +47,4 @@ Frameworks show how developers can use Tableland with existing technologies and As a database with onchain rules, Tableland can easily be used alongside other protocols. There are an endless number of ways protocols can work together, but here are some examples: - [IPFS & Filecoin](/playbooks/integrations/ipfs): Store files on IPFS, persist them on Filecoin (via web3.storage), and reference the CID in a Tableland table. +- [Lit Protocol](/playbooks/integrations/lit): Encrypt and decrypt table data with secure and decentralized key management. diff --git a/docs/playbooks/integrations/lit.md b/docs/playbooks/integrations/lit.md new file mode 100644 index 0000000..91d15ac --- /dev/null +++ b/docs/playbooks/integrations/lit.md @@ -0,0 +1,730 @@ +--- +title: Lit Protocol for encryption +description: Encrypt & decrypt table data with Lit Protocol. +keywords: + - lit + - lit protocol + - encryption + - encrypt +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +Data in Tableland is open by default, so anyone can read table data. If you want _private_ data, you can implement your own encryption scheme, such as the [example in the JETI plugin](/sdk/plugins/encryption). But, a more robust _and_ web3-native way to achieve this is with the Lit Protocol. + +## Background + +[Lit Protocol](https://www.litprotocol.com/) is a key management network for decentralized signing and encryption. The premise is that instead of a single entity holding the entire private key, each node within the network holds a unique private key share. In order to obtain the final signature or decrypted content, a pre-defined threshold for the number of nodes is required for combining signatures or decryption shares. In other words, if your threshold was 2/3 of 100 nodes, you'd need to gather decryption or signature shares from at least 67 nodes. + +Naturally, this is a perfect fit for encrypting and decrypting Tableland data! We'll walk through a simple example of how to use Lit to encrypt data before writing to Tableland, and then decrypting that data upon table reads. Note that Lit has its own built-in access control system that could be used in tandem with Tableland's access control, too. + +## Installation + +We'll first install the Tableland SDK and Lit SDK (version 3.x). This example walks uses NodeJS, and you'll also need the `siwe` package to generate a Lit `AuthSig`, described below. + +```bash npm2yarn +npm install @tableland/sdk @lit-protocol/lit-node-client siwe +``` + +:::tip +Check out the Tableland templates for a starting point: [here](/quickstarts/repos) +::: + +## Setup + +Let's first set up all of the imports that we'll be using: + + + + +```js +import { Wallet, getDefaultProvider, utils } from "ethers"; +import { + LitNodeClient, + uint8arrayFromString, +} from "@lit-protocol/lit-node-client"; +import { LIT_CHAINS } from "@lit-protocol/constants"; +import { SiweMessage } from "siwe"; +``` + + + + +```ts +import { Wallet, getDefaultProvider, utils } from "ethers"; +import { + LitNodeClient, + uint8arrayFromString, +} from "@lit-protocol/lit-node-client"; +import { LIT_CHAINS } from "@lit-protocol/constants"; +import type { AccsDefaultParams, AuthSig } from "@lit-protocol/types"; +import { SiweMessage } from "siwe"; +``` + + + + +Before we get started, we'll need to create a couple of helper methods that make it easier to work with the Lit SDK. The first one will make sure that our `chainId` maps to a Lit chain name, and the second one will create a Lit `AuthSig` for decryption. + + + + +```js +// Map a chainId to a Lit chain name via `LIT_CHAINS` +const chainIdToLitChainName = (chainId) => { + for (const [name, chain] of Object.entries(LIT_CHAINS)) { + if (chain.chainId === chainId) { + return name; + } + } + return undefined; +}; +``` + + + + +```ts +type LitChain = AccsDefaultParams["chain"]; + +// Map a chainId to a Lit chain name via `LIT_CHAINS` +const chainIdToLitChainName = (chainId: number): LitChain | undefined => { + for (const [name, chain] of Object.entries(LIT_CHAINS)) { + if (chain.chainId === chainId) { + return name as LitChain; + } + } + return undefined; +}; +``` + + + + +:::note +As of early 2024, the following Tableland testnet chains are supported by Lit, shown with their associated Lit chain name: Ethereum Sepolia (`sepolia`) and Polygon Mumbai (`mumbai`). Additionally, the following mainnet chains are supported: Filecoin (`filecoin`), Ethereum (`ethereum`), Optimism (`optimism`), Arbitrum One (`arbitrum`), and Polygon (`polygon`). +::: + +The `AuthSig` requires a Sign In With Ethereum (SIWE) message to be signed so that it can validate the address trying to decrypt the data is the correct entity. Thus, we create a `SiweMessage` with a domain, origin, statement, and expiration time. We then sign the message with the wallet and create the `AuthSig` object. + + + + +```js +// Create an authentication signature for Lit +const createAuthSig = async (client, wallet) => { + // Arbitrary domain, origin, and statement for the siwe message + const domain = "localhost"; + const origin = "http://localhost"; + const statement = "Tableland encryption"; + const expirationTime = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const nonce = client.getLatestBlockhash(); + const chainId = await wallet.getChainId(); + const address = await wallet.getAddress(); + const siweMessage = new SiweMessage({ + domain, + address, + statement, + uri: origin, + version: "1", + chainId, + nonce, + expirationTime, + }); + + // Sign the message + const messageToSign = siweMessage.prepareMessage(); + const signature = await wallet.signMessage(messageToSign); + const recoveredAddress = utils.verifyMessage(messageToSign, signature); + if (recoveredAddress !== address) { + throw new Error("recovered address does not match wallet address"); + } + + // Create the `AuthSig` compliant object for the Lit SDK + const authSig = { + sig: signature, + derivedVia: "web3.eth.personal.sign", + signedMessage: messageToSign, + address: recoveredAddress, + }; + return authSig; +}; +``` + + + + +```ts +// Create an authentication signature for Lit +const createAuthSig = async ( + client: LitNodeClient, + wallet: Wallet +): Promise => { + // Arbitrary domain, origin, and statement for the siwe message + const domain = "localhost"; + const origin = "http://localhost"; + const statement = "Tableland encryption"; + const expirationTime = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const nonce = client.getLatestBlockhash(); + const chainId = await wallet.getChainId(); + const address = await wallet.getAddress(); + const siweMessage = new SiweMessage({ + domain, + address, + statement, + uri: origin, + version: "1", + chainId, + nonce, + expirationTime, + }); + const messageToSign = siweMessage.prepareMessage(); + const signature = await wallet.signMessage(messageToSign); + const recoveredAddress = utils.verifyMessage(messageToSign, signature); + if (recoveredAddress !== address) { + throw new Error("recovered address does not match wallet address"); + } + const authSig = { + sig: signature, + derivedVia: "web3.eth.personal.sign", + signedMessage: messageToSign, + address: recoveredAddress, + }; + return authSig; +}; +``` + + + + +Now, we can set up our Tableland database connection, Lit client, and create a table! The example below shows how to do this on Ethereum Sepolia with an Alchemy provider, but you can replace this with your desired chain and provider. + +```js +// Set up a signer (note: replace with your own private key & API key) +const privateKey = "your_private_key"; +const provider = getDefaultProvider( + "https://eth-sepolia.g.alchemy.com/v2/" +); +const wallet = new Wallet(privateKey); +const signer = wallet.connect(provider); + +// Set up database and Lit client +const db = new Database({ signer }); +const client = new LitNodeClient({ debug: false }); +await client.connect(); + +// Create a table, and note that our access control will use the `tableId` as a condition +const tablePrefix = "lit_encrypt"; +const createStmt = `CREATE TABLE ${tablePrefix} (id integer primary key, msg text, hash text)`; +const { meta: create } = await db.prepare(createStmt).run(); +const tableName = create.txn?.names[0] ?? ""; +const tableId = create.txn?.tableIds[0] ?? ""; +await create.txn?.wait(); +``` + +## Writing encrypted data + +The first thing we need to do is set up our access control conditions. Lit has a variety of ways you can do this, which includes running arbitrary code or just checking the return value of standard/custom contract methods. In this example, we'll check that the signer owns a Tableland table NFT with a table ID that matches the one created above. This uses a built-in Lit method that can calls ERC721 contracts and the `ownerOf` method. If the caller _does not_ own the table, the `AuthSig` that gets uses in decryption will not be able to properly decrypt the data and throws a `Failed to decrypt` error. + + + + +```js +// Write to the table, but first encrypt the value via Lit +const chainId = await signer.getChainId(); +const chain = chainIdToLitChainName(chainId); +if (chain === undefined) { + throw new Error(`unsupported Lit chain: ${chainId}`); +} +// Create an authentication signature for Lit +const authSig = await createAuthSig(client, signer); +// Now, set up access control conditions. Here, we're checking that the signer +// owns the Tableland table NFT with table ID that matches the one created above +const tablelandContract = helpers.getContractAddress(chainId); +const accessControlConditions = [ + { + contractAddress: tablelandContract, + chain, + standardContractType: "ERC721", + method: "ownerOf", + parameters: [tableId], + returnValueTest: { + comparator: "=", + value: await signer.getAddress(), + }, + }, +]; +``` + + + + +```ts +// Write to the table, but first encrypt the value via Lit +const chainId = await signer.getChainId(); +const chain = chainIdToLitChainName(chainId); +if (chain === undefined) { + throw new Error(`unsupported Lit chain: ${chainId}`); +} +// Create an authentication signature for Lit +const authSig = await createAuthSig(client, signer); +// Now, set up access control conditions. Here, we're checking that the signer +// owns the Tableland table NFT with table ID that matches the one created above +const tablelandContract = helpers.getContractAddress(chainId); +const accessControlConditions: AccsDefaultParams[] = [ + { + contractAddress: tablelandContract, + chain, + standardContractType: "ERC721", + method: "ownerOf", + parameters: [tableId], + returnValueTest: { + comparator: "=", + value: await signer.getAddress(), + }, + }, +]; +``` + + + + +Our access control condition are now set up, so we can encrypt the data before inserting it into the table. The `ciphertext` and `dataToEncryptHash` are the encrypted data and the hash of the original data, respectively. + +```js +// Now, we can encrypt the data before inserting +const dataToEncryptStr = "this is a secret message"; +const dataToEncrypt = uint8arrayFromString(dataToEncryptStr); // Using Lit SDK helper +const { ciphertext, dataToEncryptHash } = await client.encrypt({ + authSig, + accessControlConditions, + chain, + dataToEncrypt, +}); + +// Write to the table +const writeStmt = `INSERT INTO ${tableName} (msg, hash) VALUES (?, ?)`; +const { meta: write } = await db + .prepare(writeStmt) + .bind(ciphertext, dataToEncryptHash) + .run(); +await write.txn?.wait(); +``` + +## Decrypting data + +Once our write transaction finalizes, we can read the raw data from the table and decrypt it. + + + + +```js +// Read from the table—this will have raw, encrypted data +const readStmt = `SELECT msg, hash FROM ${tableName}`; +const { results } = await db.prepare(readStmt).all(); +``` + + + + +```ts +// Read from the table—this will have raw, encrypted data +const readStmt = `SELECT msg, hash FROM ${tableName}`; +const { results } = await db + .prepare(readStmt) + .all<{ msg: string; hash: string }>(); +``` + + + + +At this point, the data looks something like this. + +```json +[ + { + "id": 1, + "msg": "rSIFVX0rCtKT6OMkWQD1TqKazrNg4B9nigsHUC/7dYkfjfW8erAZgNOHbO697gRoIVaL5Ry8GtsTsTjMyFLDMnNy6W9rmgCzgn5ALzBIUkog0VaI/NMdkCB44lUBr6EIsMdJ/2JhU8oIyLLXNv5mk+MD", + "hash": "3f98b95c16476f0b2fc37e8e664a11312966b635f60537f1f5ed75216fa0c060" + } +] +``` + +We'll need to use the access control conditions, ciphertext, hash, and authorization signature to decrypt the data (on the specified chain). + + + + +```js +// Decrypt the data read from the table, using the data read from our table, +// the access control conditions, and our authentication signature +for (const row of results) { + const { msg, hash } = row; + const { decryptedData } = await client.decrypt({ + accessControlConditions, + authSig, + chain, + ciphertext: msg, + dataToEncryptHash: hash, + }); + const decrypted = Buffer.from(decryptedData.buffer).toString(); + console.log(`Decrypted data: '${decrypted}'`); +} +``` + + + + +```ts +// Decrypt the data read from the table, using the data read from our table, +// the access control conditions, and our authentication signature +for (const row of results) { + const { msg, hash } = row; + const { decryptedData } = await client.decrypt({ + accessControlConditions, + authSig, + chain, + ciphertext: msg, + dataToEncryptHash: hash, + }); + const decrypted = Buffer.from(decryptedData.buffer).toString(); + console.log(`Decrypted data: '${decrypted}'`); +} +``` + + + + +This will output the original data that was encrypted: + +```md +Decrypted data: 'this is a secret message' +``` + +If you were to try and read the encrypted data from another wallet, all you'll be able to see is the encrypted data, and the decryption will fail with a `Failed to decrypt` error. A fun example of how the works in a dynamic fashion: use the Tableland SDK's [`Registry` class to transfer the table](/sdk/registry/#safetransferfrom) to some random wallet, and then try to read the data again. You'll see that the decryption will fail, as the new owner's address will not match the access control conditions that map to your own wallet. + +## Putting it all together + +Here's the full example with all of the helpers and code, wrapped in a `main` function to show how it all fits together: + + + + +```js +import { Database, helpers } from "@tableland/sdk"; +import { Wallet, getDefaultProvider, utils } from "ethers"; +import { + LitNodeClient, + uint8arrayFromString, +} from "@lit-protocol/lit-node-client"; +import { LIT_CHAINS } from "@lit-protocol/constants"; +import { SiweMessage } from "siwe"; + +// Map chain ID to Lit chain name +const chainIdToLitChainName = (chainId) => { + for (const [name, chain] of Object.entries(LIT_CHAINS)) { + if (chain.chainId === chainId) { + return name; + } + } + return undefined; +}; + +// Create an authentication signature for Lit +const createAuthSig = async (client, wallet) => { + // Arbitrary domain, origin, and statement for the siwe message + const domain = "localhost"; + const origin = "http://localhost"; + const statement = "Tableland encryption"; + const expirationTime = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const nonce = client.getLatestBlockhash(); + const chainId = await wallet.getChainId(); + const address = await wallet.getAddress(); + const siweMessage = new SiweMessage({ + domain, + address, + statement, + uri: origin, + version: "1", + chainId, + nonce, + expirationTime, + }); + + // Sign the message + const messageToSign = siweMessage.prepareMessage(); + const signature = await wallet.signMessage(messageToSign); + const recoveredAddress = utils.verifyMessage(messageToSign, signature); + if (recoveredAddress !== address) { + throw new Error("recovered address does not match wallet address"); + } + + // Create the `AuthSig` compliant object for the Lit SDK + const authSig = { + sig: signature, + derivedVia: "web3.eth.personal.sign", + signedMessage: messageToSign, + address: recoveredAddress, + }; + return authSig; +}; + +async function main() { + // Set up a signer (note: replace with your own private key & API key) + const privateKey = "your_private_key"; + const provider = getDefaultProvider( + "https://eth-sepolia.g.alchemy.com/v2/" + ); + const wallet = new Wallet(privateKey); + const signer = wallet.connect(provider); + + // Set up database and Lit client + const db = new Database({ signer }); + const client = new LitNodeClient({ debug: false }); + await client.connect(); + + // Create a table, and note that our access control will use the `tableId` as a condition + const tablePrefix = "lit_encrypt"; + const createStmt = `CREATE TABLE ${tablePrefix} (id integer primary key, msg text, hash text)`; + const { meta: create } = await db.prepare(createStmt).run(); + const tableName = create.txn?.names[0] ?? ""; + const tableId = create.txn?.tableIds[0] ?? ""; + await create.txn?.wait(); + + // Write to the table, but first encrypt the value via Lit + const chainId = await signer.getChainId(); + const chain = chainIdToLitChainName(chainId); + if (chain === undefined) { + throw new Error(`unsupported Lit chain: ${chainId}`); + } + // Create an authentication signature for Lit + const authSig = await createAuthSig(client, signer); + // Now, set up access control conditions. Here, we're checking that the signer + // owns the Tableland table NFT with table ID that matches the one created above + const tablelandContract = helpers.getContractAddress(chainId); + const accessControlConditions = [ + { + contractAddress: tablelandContract, + chain, + standardContractType: "ERC721", + method: "ownerOf", + parameters: [tableId], + returnValueTest: { + comparator: "=", + value: await signer.getAddress(), + }, + }, + ]; + + // Now, we can encrypt the data before inserting + const dataToEncryptStr = "this is a secret message"; + const dataToEncrypt = uint8arrayFromString(dataToEncryptStr); // Using Lit SDK helper + const { ciphertext, dataToEncryptHash } = await client.encrypt({ + authSig, + accessControlConditions, + chain, + dataToEncrypt, + }); + + // Write to the table + const writeStmt = `INSERT INTO ${tableName} (msg, hash) VALUES (?, ?)`; + const { meta: write } = await db + .prepare(writeStmt) + .bind(ciphertext, dataToEncryptHash) + .run(); + await write.txn?.wait(); + + // Read from the table—this will have raw, encrypted data + const readStmt = `SELECT msg, hash FROM ${tableName}`; + const { results } = await db.prepare(readStmt).all(); + + // Decrypt the data read from the table, using the data read from our table, + // the access control conditions, and our authentication signature + for (const row of results) { + const { msg, hash } = row; + const { decryptedData } = await client.decrypt({ + accessControlConditions, + authSig, + chain, + ciphertext: msg, + dataToEncryptHash: hash, + }); + const decrypted = Buffer.from(decryptedData.buffer).toString(); + console.log(`Decrypted data: '${decrypted}'`); + } + + process.exit(0); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); +``` + + + + +```ts +import { Database, helpers } from "@tableland/sdk"; +import { Wallet, getDefaultProvider, utils } from "ethers"; +import { + LitNodeClient, + uint8arrayFromString, +} from "@lit-protocol/lit-node-client"; +import { LIT_CHAINS } from "@lit-protocol/constants"; +import type { AccsDefaultParams, AuthSig } from "@lit-protocol/types"; +import { SiweMessage } from "siwe"; + +type LitChain = AccsDefaultParams["chain"]; + +// Map chain ID to Lit chain name +const chainIdToLitChainName = (chainId: number): LitChain | undefined => { + for (const [name, chain] of Object.entries(LIT_CHAINS)) { + if (chain.chainId === chainId) { + return name as LitChain; + } + } + return undefined; +}; + +// Create an authentication signature for Lit +const createAuthSig = async ( + client: LitNodeClient, + wallet: Wallet +): Promise => { + // Arbitrary domain, origin, and statement for the siwe message + const domain = "localhost"; + const origin = "http://localhost"; + const statement = "Tableland encryption"; + const expirationTime = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const nonce = client.getLatestBlockhash(); + const chainId = await wallet.getChainId(); + const address = await wallet.getAddress(); + const siweMessage = new SiweMessage({ + domain, + address, + statement, + uri: origin, + version: "1", + chainId, + nonce, + expirationTime, + }); + + // Sign the message + const messageToSign = siweMessage.prepareMessage(); + const signature = await wallet.signMessage(messageToSign); + const recoveredAddress = utils.verifyMessage(messageToSign, signature); + if (recoveredAddress !== address) { + throw new Error("recovered address does not match wallet address"); + } + + // Create the `AuthSig` compliant object for the Lit SDK + const authSig = { + sig: signature, + derivedVia: "web3.eth.personal.sign", + signedMessage: messageToSign, + address: recoveredAddress, + }; + return authSig; +}; + +async function main(): Promise { + // Set up a signer (note: replace with your own private key & API key) + const privateKey = "your_private_key"; + const provider = getDefaultProvider( + "https://eth-sepolia.g.alchemy.com/v2/" + ); + const wallet = new Wallet(privateKey); + const signer = wallet.connect(provider); + + // Set up database and Lit client + const db = new Database({ signer }); + const client = new LitNodeClient({ debug: false }); + await client.connect(); + + // Create a table, and note that our access control will use the `tableId` as a condition + const tablePrefix = "lit_encrypt"; + const createStmt = `CREATE TABLE ${tablePrefix} (id integer primary key, msg text, hash text)`; + const { meta: create } = await db.prepare(createStmt).run(); + const tableName = create.txn?.names[0] ?? ""; + const tableId = create.txn?.tableIds[0] ?? ""; + await create.txn?.wait(); + + // Write to the table, but first encrypt the value via Lit + const chainId = await signer.getChainId(); + const chain = chainIdToLitChainName(chainId); + if (chain === undefined) { + throw new Error(`unsupported Lit chain: ${chainId}`); + } + // Create an authentication signature for Lit + const authSig = await createAuthSig(client, signer); + // Now, set up access control conditions. Here, we're checking that the signer + // owns the Tableland table NFT with table ID that matches the one created above + const tablelandContract = helpers.getContractAddress(chainId); + const accessControlConditions: AccsDefaultParams[] = [ + { + contractAddress: tablelandContract, + chain, + standardContractType: "ERC721", + method: "ownerOf", + parameters: [tableId], + returnValueTest: { + comparator: "=", + value: await signer.getAddress(), + }, + }, + ]; + + // Now, we can encrypt the data before inserting + const dataToEncryptStr = "this is a secret message"; + const dataToEncrypt = uint8arrayFromString(dataToEncryptStr); // Using Lit SDK helper + const { ciphertext, dataToEncryptHash } = await client.encrypt({ + authSig, + accessControlConditions, + chain, + dataToEncrypt, + }); + + // Write to the table + const writeStmt = `INSERT INTO ${tableName} (msg, hash) VALUES (?, ?)`; + const { meta: write } = await db + .prepare(writeStmt) + .bind(ciphertext, dataToEncryptHash) + .run(); + await write.txn?.wait(); + + // Read from the table—this will have raw, encrypted data + const readStmt = `SELECT msg, hash FROM ${tableName}`; + const { results } = await db + .prepare(readStmt) + .all<{ msg: string; hash: string }>(); + + // Decrypt the data read from the table, using the data read from our table, + // the access control conditions, and our authentication signature + for (const row of results) { + const { msg, hash } = row; + const { decryptedData } = await client.decrypt({ + accessControlConditions, + authSig, + chain, + ciphertext: msg, + dataToEncryptHash: hash, + }); + const decrypted = Buffer.from(decryptedData.buffer).toString(); + console.log(`Decrypted data: '${decrypted}'`); + } + + process.exit(0); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); +``` + + + From 0456028550f942f3231f1a838c48a2b443e2c837 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Wed, 20 Mar 2024 11:12:44 -0700 Subject: [PATCH 7/8] fix: lit docs typo --- docs/playbooks/integrations/lit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/playbooks/integrations/lit.md b/docs/playbooks/integrations/lit.md index 91d15ac..4936bd3 100644 --- a/docs/playbooks/integrations/lit.md +++ b/docs/playbooks/integrations/lit.md @@ -21,7 +21,7 @@ Naturally, this is a perfect fit for encrypting and decrypting Tableland data! W ## Installation -We'll first install the Tableland SDK and Lit SDK (version 3.x). This example walks uses NodeJS, and you'll also need the `siwe` package to generate a Lit `AuthSig`, described below. +We'll first install the Tableland SDK and Lit SDK (version 3.x). This example uses NodeJS, and you'll also need the `siwe` package to generate a Lit `AuthSig`, described below. ```bash npm2yarn npm install @tableland/sdk @lit-protocol/lit-node-client siwe From bbd36054f08b3af37ad0d5ec4f7ce10a7ebb913a Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Wed, 20 Mar 2024 11:30:10 -0700 Subject: [PATCH 8/8] feat: gh action to trigger algolia search reindexing --- .github/workflows/algolia.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/algolia.yml diff --git a/.github/workflows/algolia.yml b/.github/workflows/algolia.yml new file mode 100644 index 0000000..7122b38 --- /dev/null +++ b/.github/workflows/algolia.yml @@ -0,0 +1,20 @@ +name: Algolia # Reindex site search on Algolia + +on: + push: + branches: + - main + +jobs: + reindex: + runs-on: ubuntu-latest + steps: + - name: Encode Algolia API credentials + run: echo "ALGOLIA_AUTH=$(echo -n ${{ secrets.CRAWLER_USER_ID }}:${{ secrets.CRAWLER_API_KEY }} | base64)" >> $GITHUB_ENV + - name: Reindex Algolia + uses: JamesIves/fetch-api-data-action@v2 + with: + endpoint: https://crawler.algolia.com/api/1/crawlers/${{ secrets.CRAWLER_ID }}/reindex + configuration: '{ "method": "POST", "headers": {"Content-Type": "application/json", "Authorization": "Basic ${ALGOLIA_AUTH}" } }' + env: + ALGOLIA_AUTH: ${{ env.ALGOLIA_AUTH }}