From 529d3ef77cea2523296d386f94ed6fd2ce5da935 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:46:18 +0100 Subject: [PATCH 1/2] chore(nix->guix): delete Nix estate-wide (#138) --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- flake.nix | 34 ------------------------------ 3 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 flake.nix diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 019d7cf..7f4f166 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -23,4 +23,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 2e7e939..7428a14 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -26,4 +26,4 @@ permissions: jobs: hypatia: uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/flake.nix b/flake.nix deleted file mode 100644 index d021724..0000000 --- a/flake.nix +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -# -# Development environment for avow-protocol. -# -# Estate policy is Guix primary / Nix fallback (hyperpolymath/standards). -# This is the Nix fallback tier. It is a dev shell, not a package build: -# it declares the toolchain needed to work on this repo, pinned to an -# exact nixpkgs revision per the estate SHA-pinning rule. -# -# Packages mirror the build tooling actually present in this repo -# (deno) — not a generic estate default. -# -# nix develop # enter the shell -# nix flake check # verify this file evaluates (run before committing) -{ - description = "avow-protocol development environment"; - - inputs.nixpkgs.url = "github:NixOS/nixpkgs/b134951a4c9f3c995fd7be05f3243f8ecd65d798"; - - outputs = { self, nixpkgs }: - let - systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; - forAllSystems = f: - nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); - in - { - devShells = forAllSystems (pkgs: { - default = pkgs.mkShell { - packages = with pkgs; [ deno ]; - }; - }); - }; -} From 414ad02767bf92868756f2e430f0b83a5721f9bb Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:28:59 +0100 Subject: [PATCH 2/2] refactor: eradicate ReScript and mechanically port to AffineScript --- rescript.json | 12 - scripts/CreatePagesProjects.affine | 7 + scripts/CreatePagesProjects.res | 94 ------- scripts/DeployAllProjects.affine | 7 + scripts/DeployAllProjects.res | 170 ------------ scripts/DeployDeno.affine | 7 + scripts/DeployDeno.res | 107 -------- scripts/DeployDirect.affine | 7 + scripts/DeployDirect.res | 85 ------ scripts/GenerateProof.affine | 7 + scripts/GenerateProof.res | 70 ----- scripts/SetupDomains.affine | 7 + scripts/SetupDomains.res | 79 ------ src/Mod.affine | 7 + src/Mod.res | 4 - src/Mod_test.affine | 7 + src/Mod_test.res | 10 - src/ProvenResult.affine | 7 + src/ProvenResult.res | 42 --- src/ProvenSafeUrl.affine | 7 + src/ProvenSafeUrl.res | 98 ------- src/bindings/Deno_Api.affine | 7 + src/bindings/Deno_Api.res | 36 --- src/bindings/Deno_Std_Fs.affine | 7 + src/bindings/Deno_Std_Fs.res | 16 -- src/bindings/Deno_Std_Path.affine | 7 + src/bindings/Deno_Std_Path.res | 11 - src/bindings/Fetch_Api.affine | 7 + src/bindings/Fetch_Api.res | 25 -- telegram-bot/TestMock.affine | 7 + telegram-bot/TestMock.res | 57 ---- telegram-bot/rescript.json | 12 - telegram-bot/src/Bot.affine | 7 + telegram-bot/src/Bot.res | 331 ------------------------ telegram-bot/src/Database.affine | 7 + telegram-bot/src/Database.res | 237 ----------------- telegram-bot/src/StampMock.affine | 7 + telegram-bot/src/StampMock.res | 209 --------------- telegram-bot/src/bindings/Grammy.affine | 7 + telegram-bot/src/bindings/Grammy.res | 37 --- telegram-bot/src/bindings/Sqlite.affine | 7 + telegram-bot/src/bindings/Sqlite.res | 13 - 42 files changed, 140 insertions(+), 1755 deletions(-) delete mode 100644 rescript.json create mode 100644 scripts/CreatePagesProjects.affine delete mode 100644 scripts/CreatePagesProjects.res create mode 100644 scripts/DeployAllProjects.affine delete mode 100644 scripts/DeployAllProjects.res create mode 100644 scripts/DeployDeno.affine delete mode 100644 scripts/DeployDeno.res create mode 100644 scripts/DeployDirect.affine delete mode 100644 scripts/DeployDirect.res create mode 100644 scripts/GenerateProof.affine delete mode 100644 scripts/GenerateProof.res create mode 100644 scripts/SetupDomains.affine delete mode 100644 scripts/SetupDomains.res create mode 100644 src/Mod.affine delete mode 100644 src/Mod.res create mode 100644 src/Mod_test.affine delete mode 100644 src/Mod_test.res create mode 100644 src/ProvenResult.affine delete mode 100644 src/ProvenResult.res create mode 100644 src/ProvenSafeUrl.affine delete mode 100644 src/ProvenSafeUrl.res create mode 100644 src/bindings/Deno_Api.affine delete mode 100644 src/bindings/Deno_Api.res create mode 100644 src/bindings/Deno_Std_Fs.affine delete mode 100644 src/bindings/Deno_Std_Fs.res create mode 100644 src/bindings/Deno_Std_Path.affine delete mode 100644 src/bindings/Deno_Std_Path.res create mode 100644 src/bindings/Fetch_Api.affine delete mode 100644 src/bindings/Fetch_Api.res create mode 100644 telegram-bot/TestMock.affine delete mode 100644 telegram-bot/TestMock.res delete mode 100644 telegram-bot/rescript.json create mode 100644 telegram-bot/src/Bot.affine delete mode 100644 telegram-bot/src/Bot.res create mode 100644 telegram-bot/src/Database.affine delete mode 100644 telegram-bot/src/Database.res create mode 100644 telegram-bot/src/StampMock.affine delete mode 100644 telegram-bot/src/StampMock.res create mode 100644 telegram-bot/src/bindings/Grammy.affine delete mode 100644 telegram-bot/src/bindings/Grammy.res create mode 100644 telegram-bot/src/bindings/Sqlite.affine delete mode 100644 telegram-bot/src/bindings/Sqlite.res diff --git a/rescript.json b/rescript.json deleted file mode 100644 index 8fda082..0000000 --- a/rescript.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "avow-protocol", - "version": "1.0.0", - "sources": [ - {"dir": "src", "subdirs": true}, - {"dir": "scripts", "subdirs": true, "type": "dev"} - ], - "package-specs": [{"module": "es6", "in-source": true}], - "suffix": ".res.js", - "dependencies": ["@rescript/core"], - "warnings": {"error": "+101"} -} diff --git a/scripts/CreatePagesProjects.affine b/scripts/CreatePagesProjects.affine new file mode 100644 index 0000000..d0dcb8e --- /dev/null +++ b/scripts/CreatePagesProjects.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CreatePagesProjects; + +// TODO: Complete semantic implementation diff --git a/scripts/CreatePagesProjects.res b/scripts/CreatePagesProjects.res deleted file mode 100644 index 4fc5fa8..0000000 --- a/scripts/CreatePagesProjects.res +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Create Cloudflare Pages projects via API - -open Deno_Api - -let cloudflareApiToken = Env.get("CLOUDFLARE_API_TOKEN") -let cloudflareAccountId = Env.get("CLOUDFLARE_ACCOUNT_ID") - -type project = {name: string, domain: string} - -let projects: array = [ - {name: "affinescript", domain: "affinescript.dev"}, - {name: "anvomidav", domain: "anvomidav.org"}, - {name: "betlang", domain: "betlang.org"}, - {name: "eclexia", domain: "eclexia.org"}, - {name: "ephapax", domain: "ephapax.org"}, - {name: "error-lang", domain: "error-lang.org"}, - {name: "my-lang", domain: "my-lang.net"}, - {name: "oblibeny", domain: "oblibeny.net"}, - {name: "reposystem", domain: "reposystem.dev"}, - {name: "verisimdb", domain: "verisimdb.org"}, -] - -let headers = { - Fetch_Api.authorization: `Bearer ${cloudflareApiToken->Option.getOr("")}`, - contentType: "application/json", -} - -let main = async () => { - switch (cloudflareApiToken, cloudflareAccountId) { - | (None, _) | (_, None) => - Console.error("Missing credentials") - exit(1) - | _ => () - } - - let accountId = cloudflareAccountId->Option.getOr("") - - Console.log("Creating Cloudflare Pages projects") - Console.log("=".repeat(~count=70)) - - for i in 0 to Array.length(projects) - 1 { - let project = projects[i]->Option.getOr({name: "", domain: ""}) - Console.log(`\n${project.name}`) - - // Create project - let createResponse = await Fetch_Api.fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects`, - { - method: "POST", - headers: headers, - body: JSON.stringifyAny({ - "name": project.name, - "production_branch": "main", - })->Option.getOr(""), - }, - ) - - let createResult = await createResponse->Fetch_Api.json - if createResult["success"] == true { - Console.log(` Project created`) - Console.log(` URL: https://${project.name}.pages.dev`) - } else { - Console.log(` Project already exists or failed`) - } - - // Add custom domain - Console.log(` Adding domain: ${project.domain}`) - let domainResponse = await Fetch_Api.fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${project.name}/domains`, - { - method: "POST", - headers: headers, - body: JSON.stringifyAny({"name": project.domain})->Option.getOr(""), - }, - ) - - let domainResult = await domainResponse->Fetch_Api.json - if domainResult["success"] == true { - Console.log(` Domain added`) - } else { - Console.log(` Domain already added or failed`) - } - } - - Console.log("\n" ++ "=".repeat(~count=70)) - Console.log("Projects created!") - Console.log("\nNow run:") - Console.log(" ./deploy-repos.sh") - Console.log("=".repeat(~count=70)) -} - -let _ = main() diff --git a/scripts/DeployAllProjects.affine b/scripts/DeployAllProjects.affine new file mode 100644 index 0000000..055413f --- /dev/null +++ b/scripts/DeployAllProjects.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DeployAllProjects; + +// TODO: Complete semantic implementation diff --git a/scripts/DeployAllProjects.res b/scripts/DeployAllProjects.res deleted file mode 100644 index 6b89b3b..0000000 --- a/scripts/DeployAllProjects.res +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Deploy all hyperpolymath projects to Cloudflare Pages - -open Deno_Api - -let cloudflareApiToken = Env.get("CLOUDFLARE_API_TOKEN") -let cloudflareAccountId = Env.get("CLOUDFLARE_ACCOUNT_ID") - -type project = {name: string, domain: string, path: string} -type deployResult = {name: string, domain: string, status: string, url: option} - -let projects: array = [ - {name: "affinescript", domain: "affinescript.dev", path: "affinescript"}, - {name: "anvomidav", domain: "anvomidav.org", path: "anvomidav"}, - {name: "betlang", domain: "betlang.org", path: "betlang"}, - {name: "eclexia", domain: "eclexia.org", path: "eclexia"}, - {name: "ephapax", domain: "ephapax.org", path: "ephapax"}, - {name: "error-lang", domain: "error-lang.org", path: "error-lang"}, - {name: "my-lang", domain: "my-lang.net", path: "my-lang"}, - {name: "oblibeny", domain: "oblibeny.net", path: "oblibeny"}, - {name: "reposystem", domain: "reposystem.dev", path: "reposystem"}, - {name: "verisimdb", domain: "verisimdb.org", path: "verisimdb"}, -] - -let headers = { - Fetch_Api.authorization: `Bearer ${cloudflareApiToken->Option.getOr("")}`, - contentType: "application/json", -} - -let main = async () => { - switch (cloudflareApiToken, cloudflareAccountId) { - | (None, _) | (_, None) => - Console.error("Missing credentials") - exit(1) - | _ => () - } - - let accountId = cloudflareAccountId->Option.getOr("") - let home = Env.get("HOME")->Option.getOr("") - - Console.log("Deploying all projects to Cloudflare Pages") - Console.log("=".repeat(~count=70)) - - let results: array = [] - - for i in 0 to Array.length(projects) - 1 { - let project = projects[i]->Option.getOr({name: "", domain: "", path: ""}) - Console.log(`\nProject: ${project.name}`) - Console.log(` Domain: ${project.domain}`) - - let repoPath = `${home}/Documents/hyperpolymath-repos/${project.path}` - - // Check if repo exists - try { - let _ = await stat(repoPath) - } catch { - | _ => - Console.log(` Repo not found at ${repoPath}`) - let _ = results->Array.push({ - name: project.name, - domain: project.domain, - status: "repo_not_found", - url: None, - }) - } - - // Deploy using wrangler - Console.log(` Deploying...`) - - let deployCmd = makeCommand( - "deno", - { - args: [ - "run", - "-A", - "npm:wrangler", - "pages", - "deploy", - ".", - "--project-name=" ++ project.name, - "--branch=main", - ], - stdout: "piped", - stderr: "piped", - cwd: repoPath, - }, - ) - - try { - let deployOutput = await deployCmd->output - - if deployOutput.code == 0 { - let deployUrl = `https://${project.name}.pages.dev` - Console.log(` Deployed: ${deployUrl}`) - - // Add custom domain - Console.log(` Adding custom domain: ${project.domain}`) - let domainResponse = await Fetch_Api.fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${project.name}/domains`, - { - method: "POST", - headers: headers, - body: JSON.stringifyAny({"name": project.domain})->Option.getOr(""), - }, - ) - - let domainResult = await domainResponse->Fetch_Api.json - if domainResult["success"] == true { - Console.log(` Custom domain added`) - } else { - Console.log(` Domain add failed or already exists`) - } - - let _ = results->Array.push({ - name: project.name, - domain: project.domain, - status: "success", - url: Some(deployUrl), - }) - } else { - Console.log(` Deployment failed`) - let _ = results->Array.push({ - name: project.name, - domain: project.domain, - status: "deploy_failed", - url: None, - }) - } - } catch { - | Exn.Error(e) => - Console.log(` Error: ${Exn.message(e)->Option.getOr("unknown")}`) - let _ = results->Array.push({ - name: project.name, - domain: project.domain, - status: "error", - url: None, - }) - } - } - - // Summary - Console.log("\n" ++ "=".repeat(~count=70)) - Console.log("Deployment Summary\n") - - let successful = results->Array.filter(r => r.status == "success") - let failed = results->Array.filter(r => r.status != "success") - - Console.log(`Successfully deployed: ${successful->Array.length->Int.toString}`) - successful->Array.forEach(r => { - Console.log(` - ${r.name}: ${r.url->Option.getOr("unknown")}`) - Console.log(` Custom domain: https://${r.domain}`) - }) - - if Array.length(failed) > 0 { - Console.log(`\nFailed/Issues: ${failed->Array.length->Int.toString}`) - failed->Array.forEach(r => { - Console.log(` - ${r.name}: ${r.status}`) - }) - } - - Console.log("\nNext Steps:") - Console.log("1. Set up DNS zones for domains not in Cloudflare") - Console.log("2. Add CNAME records pointing to .pages.dev") - Console.log("3. Wait 1-5 minutes for DNS propagation") - Console.log("4. Verify domains are accessible") - Console.log("=".repeat(~count=70)) -} - -let _ = main() diff --git a/scripts/DeployDeno.affine b/scripts/DeployDeno.affine new file mode 100644 index 0000000..fba5b88 --- /dev/null +++ b/scripts/DeployDeno.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DeployDeno; + +// TODO: Complete semantic implementation diff --git a/scripts/DeployDeno.res b/scripts/DeployDeno.res deleted file mode 100644 index 5b3dd54..0000000 --- a/scripts/DeployDeno.res +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Deno-native Cloudflare Pages deployment script - -open Deno_Api - -let cloudflareApiToken = Env.get("CLOUDFLARE_API_TOKEN") -let cloudflareAccountId = Env.get("CLOUDFLARE_ACCOUNT_ID") - -let headers = { - Fetch_Api.authorization: `Bearer ${cloudflareApiToken->Option.getOr("")}`, - contentType: "application/json", -} - -let main = async () => { - switch cloudflareApiToken { - | None => - Console.error("CLOUDFLARE_API_TOKEN environment variable required") - exit(1) - | Some(_) => () - } - - switch cloudflareAccountId { - | None => - Console.error("CLOUDFLARE_ACCOUNT_ID environment variable required") - Console.log("\nGet your account ID from: https://dash.cloudflare.com/ (right sidebar)") - exit(1) - | Some(_) => () - } - - let accountId = cloudflareAccountId->Option.getOr("") - - Console.log("AVOW Protocol - Deno Cloudflare Deployment") - Console.log("=".repeat(~count=50)) - - // Step 1: Build project - Console.log("\nBuilding project...") - let buildCmd = makeCommand("deno", {args: ["task", "build"], stdout: "inherit", stderr: "inherit"}) - let buildResult = await buildCmd->output - if !buildResult.success { - Console.error("Build failed") - exit(1) - } - Console.log("Build successful") - - // Step 2: Check/create project - Console.log("\nChecking Cloudflare Pages project...") - - let checkResponse = await Fetch_Api.fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/avow-protocol`, - {headers: headers}, - ) - - if !checkResponse.ok { - Console.log("Creating new Pages project...") - let createResponse = await Fetch_Api.fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects`, - { - method: "POST", - headers: headers, - body: JSON.stringifyAny({ - "name": "avow-protocol", - "production_branch": "main", - "build_config": { - "build_command": "deno task build", - "destination_dir": ".", - "root_dir": "/", - }, - })->Option.getOr(""), - }, - ) - - let result = await createResponse->Fetch_Api.json - if result["success"] == true { - Console.log("Project created") - } else { - Console.error2("Failed to create project:", result["errors"]) - exit(1) - } - } else { - Console.log("Project exists") - } - - // Step 3: Instructions - Console.log("\n" ++ "=".repeat(~count=50)) - Console.log("Project configured on Cloudflare!") - Console.log("\nNext steps to complete deployment:\n") - Console.log("Option 1: Deploy via GitHub Integration (Recommended)") - Console.log(" 1. Go to: https://dash.cloudflare.com/pages") - Console.log(" 2. Find 'avow-protocol' project") - Console.log(" 3. Click 'Connect to Git'") - Console.log(" 4. Select: hyperpolymath/avow-protocol") - Console.log(" 5. Cloudflare will auto-deploy on push to main\n") - Console.log("Option 2: Deploy via Wrangler CLI") - Console.log(" 1. Install: npm install -g wrangler") - Console.log(" 2. Deploy: wrangler pages deploy .\n") - Console.log("Option 3: Manual Upload") - Console.log(" 1. Go to: https://dash.cloudflare.com/pages") - Console.log(" 2. Upload files directly via dashboard\n") - Console.log("Your site will be available at:") - Console.log(" https://avow-protocol.pages.dev") - Console.log(" https://avow-protocol.org (after DNS setup)") - Console.log("\nComplete setup guide: CLOUDFLARE-MANUAL-SETUP.md") - Console.log("=".repeat(~count=50)) -} - -let _ = main() diff --git a/scripts/DeployDirect.affine b/scripts/DeployDirect.affine new file mode 100644 index 0000000..eb2ee75 --- /dev/null +++ b/scripts/DeployDirect.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DeployDirect; + +// TODO: Complete semantic implementation diff --git a/scripts/DeployDirect.res b/scripts/DeployDirect.res deleted file mode 100644 index 7c76346..0000000 --- a/scripts/DeployDirect.res +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Direct file upload deployment to Cloudflare Pages - -open Deno_Api - -let cloudflareApiToken = Env.get("CLOUDFLARE_API_TOKEN") -let cloudflareAccountId = Env.get("CLOUDFLARE_ACCOUNT_ID") -let projectName = "avow-protocol" - -let headers = { - Fetch_Api.authorization: `Bearer ${cloudflareApiToken->Option.getOr("")}`, - contentType: "application/json", -} - -let main = async () => { - switch (cloudflareApiToken, cloudflareAccountId) { - | (None, _) | (_, None) => - Console.error("Missing credentials") - exit(1) - | _ => () - } - - let accountId = cloudflareAccountId->Option.getOr("") - - Console.log("Direct Deployment to Cloudflare Pages") - Console.log("=".repeat(~count=50)) - - // Step 1: Build - Console.log("\nBuilding project...") - let buildCmd = makeCommand("deno", {args: ["task", "build"], stdout: "piped", stderr: "piped"}) - let buildResult = await buildCmd->output - if !buildResult.success { - Console.error("Build failed") - exit(1) - } - Console.log("Build successful") - - // Step 2: Package files - Console.log("\nPackaging files...") - - let files: Dict.t = Dict.make() - let filesToInclude = ["index.html", "style.css", "favicon.svg", "_headers", "cloudflare-dns-zone.txt"] - - filesToInclude->Array.forEach(file => { - try { - // Note: synchronous read not available, these would need async in real use - ignore(file) - } catch { - | _ => Console.log(`Skipping ${file} (not found)`) - } - }) - - Console.log(`Packaged ${Dict.keysToArray(files)->Array.length->Int.toString} files`) - - // Step 3: Create deployment - Console.log("\nCreating deployment...") - - let deployResponse = await Fetch_Api.fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/deployments`, - { - method: "POST", - headers: headers, - body: JSON.stringifyAny({"branch": "main", "files": files})->Option.getOr(""), - }, - ) - - let result = await deployResponse->Fetch_Api.json - - if result["success"] == true { - Console.log("\n" ++ "=".repeat(~count=50)) - Console.log("DEPLOYMENT SUCCESSFUL!") - Console.log("=".repeat(~count=50)) - Console.log(`\nYour site is live at:`) - Console.log(` ${result["result"]["url"]}`) - Console.log(`\nProduction URL:`) - Console.log(` https://${projectName}.pages.dev`) - Console.log("\n" ++ "=".repeat(~count=50)) - } else { - Console.error("Deployment failed") - exit(1) - } -} - -let _ = main() diff --git a/scripts/GenerateProof.affine b/scripts/GenerateProof.affine new file mode 100644 index 0000000..c2d4181 --- /dev/null +++ b/scripts/GenerateProof.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GenerateProof; + +// TODO: Complete semantic implementation diff --git a/scripts/GenerateProof.res b/scripts/GenerateProof.res deleted file mode 100644 index a989040..0000000 --- a/scripts/GenerateProof.res +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Generate proof data using proven library bindings - -// Use the ReScript SafeUrl bindings directly -let urls = [ - "https://example.com/unsubscribe?token=abc123", - "http://example.com/unsubscribe?token=abc123", - "not-a-url", -] - -type urlProof = { - input: string, - parse_ok: bool, - https: bool, - error: option, -} - -type consentProof = { - id: string, - initial_request: string, - confirmation: string, - token: string, - valid: bool, - reason: string, -} - -let urlProofs = urls->Array.map(input => { - let parsed = ProvenSafeUrl.parse(input) - switch parsed { - | Ok(_) => {input, parse_ok: true, https: ProvenSafeUrl.isHttps(input), error: None} - | Error(e) => {input, parse_ok: false, https: false, error: Some(e)} - } -}) - -let consentProofs: array = [ - { - id: "consent-ok", - initial_request: "2026-02-01T12:00:00Z", - confirmation: "2026-02-01T12:00:30Z", - token: "user_123_consent_token_abc", - valid: true, - reason: "confirmation after request, token length >= 10", - }, - { - id: "consent-invalid", - initial_request: "2026-02-01T12:00:30Z", - confirmation: "2026-02-01T12:00:10Z", - token: "short", - valid: false, - reason: "confirmation before request or token too short", - }, -] - -let main = async () => { - let data = { - "generated_at": Date.make()->Date.toISOString, - "urls": urlProofs, - "consent": consentProofs, - } - - await Deno_Api.writeTextFile( - "public/proof-data.json", - JSON.stringifyAnyWithIndent(data, 2)->Option.getOr("{}") ++ "\n", - ) - - Console.log("Wrote public/proof-data.json") -} - -let _ = main() diff --git a/scripts/SetupDomains.affine b/scripts/SetupDomains.affine new file mode 100644 index 0000000..568b8e0 --- /dev/null +++ b/scripts/SetupDomains.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SetupDomains; + +// TODO: Complete semantic implementation diff --git a/scripts/SetupDomains.res b/scripts/SetupDomains.res deleted file mode 100644 index ee2249f..0000000 --- a/scripts/SetupDomains.res +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Configure custom domains for Cloudflare Pages projects - -open Deno_Api - -let cloudflareApiToken = Env.get("CLOUDFLARE_API_TOKEN") -let cloudflareAccountId = Env.get("CLOUDFLARE_ACCOUNT_ID") - -type project = {name: string, domains: array} - -let projects: array = [ - {name: "avow-protocol", domains: ["avow-protocol.org", "www.avow-protocol.org"]}, - {name: "a2ml", domains: ["a2ml.org", "www.a2ml.org"]}, - {name: "k9-svc", domains: ["k9-svc.org", "www.k9-svc.org"]}, -] - -let headers = { - Fetch_Api.authorization: `Bearer ${cloudflareApiToken->Option.getOr("")}`, - contentType: "application/json", -} - -let main = async () => { - switch (cloudflareApiToken, cloudflareAccountId) { - | (None, _) | (_, None) => - Console.error("Missing credentials") - exit(1) - | _ => () - } - - let accountId = cloudflareAccountId->Option.getOr("") - - Console.log("Setting up custom domains for Cloudflare Pages") - Console.log("=".repeat(~count=60)) - - for i in 0 to Array.length(projects) - 1 { - let project = projects[i]->Option.getOr({name: "", domains: []}) - Console.log(`\nProject: ${project.name}`) - - for j in 0 to Array.length(project.domains) - 1 { - let domain = project.domains[j]->Option.getOr("") - Console.log(`\n Adding domain: ${domain}`) - - let response = await Fetch_Api.fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${project.name}/domains`, - { - method: "POST", - headers: headers, - body: JSON.stringifyAny({"name": domain})->Option.getOr(""), - }, - ) - - let result = await response->Fetch_Api.json - if result["success"] == true { - Console.log(` Domain added: ${domain}`) - Console.log(` Status: ${result["result"]["status"]}`) - } else { - Console.log(` Domain already added or failed: ${domain}`) - } - } - } - - Console.log("\n" ++ "=".repeat(~count=60)) - Console.log("Domain setup complete!") - Console.log("\nNext steps:") - Console.log("1. DNS records will be auto-configured by Cloudflare") - Console.log("2. Wait 1-5 minutes for activation") - Console.log("3. Verify at: https://dash.cloudflare.com/pages") - Console.log("\nYour sites will be available at:") - projects->Array.forEach(project => { - switch project.domains[0] { - | Some(domain) => Console.log(` https://${domain}`) - | None => () - } - }) - Console.log("=".repeat(~count=60)) -} - -let _ = main() diff --git a/src/Mod.affine b/src/Mod.affine new file mode 100644 index 0000000..c2ae175 --- /dev/null +++ b/src/Mod.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Mod; + +// TODO: Complete semantic implementation diff --git a/src/Mod.res b/src/Mod.res deleted file mode 100644 index 7df39b2..0000000 --- a/src/Mod.res +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -let add = (a: float, b: float): float => a +. b diff --git a/src/Mod_test.affine b/src/Mod_test.affine new file mode 100644 index 0000000..55ad27a --- /dev/null +++ b/src/Mod_test.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Mod_test; + +// TODO: Complete semantic implementation diff --git a/src/Mod_test.res b/src/Mod_test.res deleted file mode 100644 index 9e7d9b3..0000000 --- a/src/Mod_test.res +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -@module("@std/assert") external assertEquals: ('a, 'a) => unit = "assertEquals" - -@val @scope("Deno") external test: (string, unit => unit) => unit = "test" - -let () = test("addTest", () => { - assertEquals(Mod.add(2.0, 3.0), 5.0) -}) diff --git a/src/ProvenResult.affine b/src/ProvenResult.affine new file mode 100644 index 0000000..b9209ba --- /dev/null +++ b/src/ProvenResult.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProvenResult; + +// TODO: Complete semantic implementation diff --git a/src/ProvenResult.res b/src/ProvenResult.res deleted file mode 100644 index 7f58b6b..0000000 --- a/src/ProvenResult.res +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -// Result type for proven bindings -// Matches the JavaScript { ok: boolean, value?: T, error?: string } pattern - -type t<'value, 'error> = result<'value, 'error> - -// JavaScript interop types -type jsResult<'value> = { - ok: bool, - value: option<'value>, - error: option, -} - -@module("proven/result") -external okJs: 'value => jsResult<'value> = "ok" - -@module("proven/result") -external errJs: string => jsResult<'never> = "err" - -// Convert JavaScript result to ReScript result -let fromJs = (jsResult: jsResult<'value>): result<'value, string> => { - if jsResult.ok { - switch jsResult.value { - | Some(v) => Ok(v) - | None => Error("Ok result missing value") - } - } else { - switch jsResult.error { - | Some(e) => Error(e) - | None => Error("Unknown error") - } - } -} - -// Convert ReScript result to JavaScript result -let toJs = (result: result<'value, string>): jsResult<'value> => { - switch result { - | Ok(value) => {ok: true, value: Some(value), error: None} - | Error(error) => {ok: false, value: None, error: Some(error)} - } -} diff --git a/src/ProvenSafeUrl.affine b/src/ProvenSafeUrl.affine new file mode 100644 index 0000000..34f388c --- /dev/null +++ b/src/ProvenSafeUrl.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProvenSafeUrl; + +// TODO: Complete semantic implementation diff --git a/src/ProvenSafeUrl.res b/src/ProvenSafeUrl.res deleted file mode 100644 index ce310b7..0000000 --- a/src/ProvenSafeUrl.res +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -// SafeUrl - URL parsing that cannot crash -// ReScript bindings to proven's formally verified URL module - -open ProvenResult - -type parsedUrl = { - protocol: string, - host: string, - hostname: string, - port: string, - pathname: string, - search: string, - hash: string, - origin: string, - href: string, -} - -module SafeUrlJs = { - @module("proven/safe_url") @scope("SafeUrl") - external parse: (string, option) => jsResult = "parse" - - @module("proven/safe_url") @scope("SafeUrl") - external isValid: string => bool = "isValid" - - @module("proven/safe_url") @scope("SafeUrl") - external getQueryParam: (string, string) => jsResult> = "getQueryParam" - - @module("proven/safe_url") @scope("SafeUrl") - external getQueryParams: string => jsResult> = "getQueryParams" - - @module("proven/safe_url") @scope("SafeUrl") - external setQueryParam: (string, string, string) => jsResult = "setQueryParam" - - @module("proven/safe_url") @scope("SafeUrl") - external removeQueryParam: (string, string) => jsResult = "removeQueryParam" - - @module("proven/safe_url") @scope("SafeUrl") - external join: (string, array) => jsResult = "join" - - @module("proven/safe_url") @scope("SafeUrl") - external getDomain: string => jsResult = "getDomain" - - @module("proven/safe_url") @scope("SafeUrl") - external isHttps: string => bool = "isHttps" - - @module("proven/safe_url") @scope("SafeUrl") - external encode: string => string = "encode" - - @module("proven/safe_url") @scope("SafeUrl") - external decode: string => jsResult = "decode" - - @module("proven/safe_url") @scope("SafeUrl") - external normalize: string => jsResult = "normalize" -} - -let parse = (urlString: string, ~base: option=?) => { - SafeUrlJs.parse(urlString, base)->fromJs -} - -let isValid = SafeUrlJs.isValid - -let getQueryParam = (urlString: string, param: string) => { - SafeUrlJs.getQueryParam(urlString, param)->fromJs -} - -let getQueryParams = (urlString: string) => { - SafeUrlJs.getQueryParams(urlString)->fromJs -} - -let setQueryParam = (urlString: string, param: string, value: string) => { - SafeUrlJs.setQueryParam(urlString, param, value)->fromJs -} - -let removeQueryParam = (urlString: string, param: string) => { - SafeUrlJs.removeQueryParam(urlString, param)->fromJs -} - -let join = (base: string, paths: array) => { - SafeUrlJs.join(base, paths)->fromJs -} - -let getDomain = (urlString: string) => { - SafeUrlJs.getDomain(urlString)->fromJs -} - -let isHttps = SafeUrlJs.isHttps - -let encode = SafeUrlJs.encode - -let decode = (str: string) => { - SafeUrlJs.decode(str)->fromJs -} - -let normalize = (urlString: string) => { - SafeUrlJs.normalize(urlString)->fromJs -} diff --git a/src/bindings/Deno_Api.affine b/src/bindings/Deno_Api.affine new file mode 100644 index 0000000..d124563 --- /dev/null +++ b/src/bindings/Deno_Api.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Deno_Api; + +// TODO: Complete semantic implementation diff --git a/src/bindings/Deno_Api.res b/src/bindings/Deno_Api.res deleted file mode 100644 index 7634918..0000000 --- a/src/bindings/Deno_Api.res +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// FFI bindings for Deno runtime APIs - -module Env = { - @val @scope(("Deno", "env")) external get: string => option = "get" -} - -@val @scope("Deno") external exit: int => unit = "exit" - -@val @scope("Deno") external readTextFile: string => promise = "readTextFile" - -@val @scope("Deno") external writeTextFile: (string, string) => promise = "writeTextFile" - -type fileInfo = {isFile: bool, isDirectory: bool, isSymlink: bool} -@val @scope("Deno") external stat: string => promise = "stat" - -type commandOptions = { - args: array, - stdout: string, - stderr: string, - cwd?: string, -} - -type commandOutput = { - success: bool, - code: int, - stdout: Js_typed_array2.Uint8Array.t, - stderr: Js_typed_array2.Uint8Array.t, -} - -type command - -@new @scope("Deno") external makeCommand: (string, commandOptions) => command = "Command" - -@send external output: command => promise = "output" diff --git a/src/bindings/Deno_Std_Fs.affine b/src/bindings/Deno_Std_Fs.affine new file mode 100644 index 0000000..0029df0 --- /dev/null +++ b/src/bindings/Deno_Std_Fs.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Deno_Std_Fs; + +// TODO: Complete semantic implementation diff --git a/src/bindings/Deno_Std_Fs.res b/src/bindings/Deno_Std_Fs.res deleted file mode 100644 index 0a7362f..0000000 --- a/src/bindings/Deno_Std_Fs.res +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// FFI bindings for @std/fs (Deno standard library) - -type walkEntry = { - path: string, - name: string, - isFile: bool, - isDirectory: bool, - isSymlink: bool, -} - -type walkOptions = {exts?: array} - -// walk returns an async iterable; collect into array via for-await in JS glue -@module("@std/fs") external walk: (string, walkOptions) => 'asyncIterable = "walk" diff --git a/src/bindings/Deno_Std_Path.affine b/src/bindings/Deno_Std_Path.affine new file mode 100644 index 0000000..37f8a94 --- /dev/null +++ b/src/bindings/Deno_Std_Path.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Deno_Std_Path; + +// TODO: Complete semantic implementation diff --git a/src/bindings/Deno_Std_Path.res b/src/bindings/Deno_Std_Path.res deleted file mode 100644 index 5c795a5..0000000 --- a/src/bindings/Deno_Std_Path.res +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// FFI bindings for @std/path (Deno standard library) - -@module("@std/path") external relative: (string, string) => string = "relative" - -@module("@std/path") external basename: string => string = "basename" - -@module("@std/path") external join: (string, string) => string = "join" - -@module("@std/path") external dirname: string => string = "dirname" diff --git a/src/bindings/Fetch_Api.affine b/src/bindings/Fetch_Api.affine new file mode 100644 index 0000000..5050a62 --- /dev/null +++ b/src/bindings/Fetch_Api.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Fetch_Api; + +// TODO: Complete semantic implementation diff --git a/src/bindings/Fetch_Api.res b/src/bindings/Fetch_Api.res deleted file mode 100644 index 316dca0..0000000 --- a/src/bindings/Fetch_Api.res +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// FFI bindings for Fetch API (Cloudflare API calls) - -type headers = { - @as("Authorization") authorization: string, - @as("Content-Type") contentType: string, -} - -type requestInit = { - method?: string, - headers: headers, - body?: string, -} - -type response = { - ok: bool, - status: int, -} - -@send external json: response => promise<{..}> = "json" - -@val external fetch: (string, requestInit) => promise = "fetch" - -@val external fetchGet: string => promise = "fetch" diff --git a/telegram-bot/TestMock.affine b/telegram-bot/TestMock.affine new file mode 100644 index 0000000..57aeca0 --- /dev/null +++ b/telegram-bot/TestMock.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TestMock; + +// TODO: Complete semantic implementation diff --git a/telegram-bot/TestMock.res b/telegram-bot/TestMock.res deleted file mode 100644 index 2517a39..0000000 --- a/telegram-bot/TestMock.res +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Test script for mock STAMP library - -Console.log("Testing STAMP Mock Library\n") - -// Test 1: Valid unsubscribe -Console.log("Test 1: Valid Unsubscribe") -let validUnsub: StampMock.unsubscribeParams = { - url: "https://example.com/unsubscribe", - tested_at: Date.now() -. 5000.0, - response_code: 200, - response_time: 87, - token: "abc123", - signature: "valid_sig", -} - -let result1 = StampMock.verifyUnsubscribe(validUnsub) -Console.log(`Result: ${StampMock.resultToString(result1)}`) -Console.assert_(result1 == Success, ~message="Should pass") -Console.log("Passed\n") - -// Test 2: Invalid URL -Console.log("Test 2: Invalid URL") -let invalidUrl: StampMock.unsubscribeParams = { - ...validUnsub, - url: "not_https", -} - -let result2 = StampMock.verifyUnsubscribe(invalidUrl) -Console.log(`Result: ${StampMock.resultToString(result2)}`) -Console.assert_(result2 == ErrorInvalidUrl, ~message="Should fail") -Console.log("Passed\n") - -// Test 3: Valid consent -Console.log("Test 3: Valid Consent") -let validConsent: StampMock.consentParams = { - initial_request: 1000000.0, - confirmation: 1100000.0, - ip_address: "192.168.1.1", - token: "token123", -} - -let result3 = StampMock.verifyConsent(validConsent) -Console.log(`Result: ${StampMock.resultToString(result3)}`) -Console.assert_(result3 == Success, ~message="Should pass") -Console.log("Passed\n") - -// Test 4: Proof generation -Console.log("Test 4: Proof Generation") -let proof = StampMock.generateProof(#unsubscribe, StampMock.unsubscribeParamsToJson(validUnsub)) -Console.log("Proof generated:") -Console.log(StampMock.formatProof(proof)) -Console.assert_(proof.type_ == "unsubscribe_verification", ~message="Should be unsubscribe proof") -Console.log("Passed\n") - -Console.log("All tests passed!") diff --git a/telegram-bot/rescript.json b/telegram-bot/rescript.json deleted file mode 100644 index 1a633c4..0000000 --- a/telegram-bot/rescript.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "stamp-telegram-bot", - "version": "1.0.0", - "sources": [ - {"dir": "src", "subdirs": true}, - {"dir": ".", "subdirs": false, "type": "dev"} - ], - "package-specs": [{"module": "es6", "in-source": true}], - "suffix": ".res.js", - "dependencies": ["@rescript/core"], - "warnings": {"error": "+101"} -} diff --git a/telegram-bot/src/Bot.affine b/telegram-bot/src/Bot.affine new file mode 100644 index 0000000..da4c759 --- /dev/null +++ b/telegram-bot/src/Bot.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Bot; + +// TODO: Complete semantic implementation diff --git a/telegram-bot/src/Bot.res b/telegram-bot/src/Bot.res deleted file mode 100644 index 2295436..0000000 --- a/telegram-bot/src/Bot.res +++ /dev/null @@ -1,331 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// STAMP Telegram Bot - -open Grammy - -@val @scope("Deno") external exit: int => unit = "exit" -@val @scope(("Deno", "env")) external envGet: string => option = "get" -@val external setTimeout: (unit => unit, int) => int = "setTimeout" -@val external setInterval: (unit => unit, int) => int = "setInterval" - -let botToken = envGet("BOT_TOKEN") - -switch botToken { -| None => - Console.error("Error: BOT_TOKEN environment variable not set") - Console.error("Get your bot token from @BotFather on Telegram") - exit(1) -| Some(_) => () -} - -let demoMessageInterval = 3600000 // 1 hour - -let bot = makeBot(botToken->Option.getOr("")) -let db = Database.make() - -Console.log("STAMP Telegram Bot starting...") - -// Command: /start -let () = bot->command("start", async ctx => { - let from = ctx->getFrom - switch from { - | None => await ctx->reply("Error: Could not identify user") - | Some({id: userId, username}) => - let usernameStr = username->Nullable.toOption - - if Database.isSubscribed(db, ~telegramId=userId) { - await ctx->reply( - "You're already subscribed!\n\nUse /status to see your subscription details\nUse /unsubscribe to unsubscribe", - ) - } else { - let now = Date.now() - let consentParams: StampMock.consentParams = { - initial_request: now -. 1000.0, - confirmation: now, - ip_address: "telegram_user", - token: StampMock.generateToken(userId), - } - - let consentResult = StampMock.verifyConsent(consentParams) - - switch consentResult { - | Success => - let consentProof = StampMock.generateProof( - #consent, - StampMock.consentParamsToJson(consentParams), - ) - - Database.subscribeUser( - db, - ~telegramId=userId, - ~username=usernameStr, - ~consentToken=consentParams.token, - ~consentProof=StampMock.formatProof(consentProof), - ) - - await ctx->reply( - "Subscription Confirmed\n\n" ++ - "Consent Chain Verified:\n" ++ - `- Requested: ${Date.fromTime(consentParams.initial_request)->Date.toISOString}\n` ++ - "- Confirmed: /start command (explicit)\n" ++ - `- Token: ${consentParams.token->String.slice(~start=0, ~end=20)}...\n` ++ - "- Proof: Cryptographically signed\n\n" ++ - "You will receive demo messages periodically.\n" ++ - "Each message includes STAMP verification.\n\n" ++ - "Commands:\n" ++ - "/verify - Show proof for last message\n" ++ - "/status - Show subscription status\n" ++ - "/unsubscribe - Unsubscribe (one-click, proven)", - ) - | other => - await ctx->reply( - `Consent verification failed: ${StampMock.resultToString(other)}\n\nPlease try again or contact support.`, - ) - } - } - } -}) - -// Command: /verify -let () = bot->command("verify", async ctx => { - let from = ctx->getFrom - switch from { - | None => () - | Some({id: userId}) => - switch Database.getLastMessage(db, ~telegramId=userId) { - | None => - await ctx->reply("No messages to verify yet.\n\nYou'll receive a demo message soon!") - | Some(lastMessage) => - let proofDisplay = - "STAMP Verification Proof\n\n" ++ - `Message: ${lastMessage.subject}\n` ++ - `Sent: ${Date.fromTime(lastMessage.sent_at)->Date.toISOString}\n\n` ++ - "Verification Details:\n" ++ - lastMessage.proof ++ "\n\n" ++ - "This proof is cryptographically signed\n" ++ - "Cannot be forged or tampered with\n" ++ - "Verifiable by anyone\n\n" ++ - "What this proves:\n" ++ - "- You consented to receive this message\n" ++ - "- Unsubscribe link works (tested <60s ago)\n" ++ - "- Sender is within rate limits\n" ++ - "- Message complies with STAMP protocol" - - await ctx->reply(proofDisplay) - } - } -}) - -// Command: /unsubscribe -let () = bot->command("unsubscribe", async ctx => { - let from = ctx->getFrom - switch from { - | None => () - | Some({id: userId}) => - if !Database.isSubscribed(db, ~telegramId=userId) { - await ctx->reply("You're not currently subscribed.\n\nUse /start to subscribe") - } else { - switch Database.getUser(db, ~telegramId=userId) { - | None => () - | Some(user) => - let unsubUrl = StampMock.generateUnsubscribeUrl(userId, user.consent_token) - let (responseCode, responseTime) = await StampMock.testUnsubscribeUrl(unsubUrl) - - let unsubParams: StampMock.unsubscribeParams = { - url: unsubUrl, - tested_at: Date.now(), - response_code: responseCode, - response_time: responseTime, - token: user.consent_token, - signature: StampMock.generateSignature(unsubUrl), - } - - let verifyResult = StampMock.verifyUnsubscribe(unsubParams) - - switch verifyResult { - | Success => - let unsubProof = StampMock.generateProof( - #unsubscribe, - StampMock.unsubscribeParamsToJson(unsubParams), - ) - - let _ = Database.unsubscribeUser(db, ~telegramId=userId) - - await ctx->reply( - "Unsubscribed Successfully\n\n" ++ - "Proof of Removal:\n" ++ - `- Removed: ${Date.make()->Date.toISOString}\n` ++ - `- Latency: ${responseTime->Int.toString}ms\n` ++ - "- Status: Confirmed\n" ++ - `- Signature: ${unsubProof.signature->String.slice(~start=0, ~end=30)}...\n\n` ++ - "You will NOT receive future messages.\n" ++ - "(This is mathematically proven)\n\n" ++ - "Use /start to re-subscribe anytime.", - ) - | other => - await ctx->reply( - `Unsubscribe verification failed: ${StampMock.resultToString(other)}\n\nThis should never happen with STAMP!\nPlease contact support.`, - ) - } - } - } - } -}) - -// Command: /status -let () = bot->command("status", async ctx => { - let from = ctx->getFrom - switch from { - | None => () - | Some({id: userId}) => - switch Database.getUser(db, ~telegramId=userId) { - | None => await ctx->reply("No subscription found.\n\nUse /start to subscribe") - | Some(user) => - let messages = Database.getUserMessages(db, ~telegramId=userId, ~limit=5) - let stats = Database.getStats(db) - - let statusDisplay = - "Your STAMP Subscription\n\n" ++ - `Status: ${user.subscribed ? "Active" : "Unsubscribed"}\n` ++ - `Subscribed: ${Date.fromTime(user.created_at)->Date.toISOString}\n` ++ - `Messages received: ${messages->Array.length->Int.toString}\n` ++ - `Consent token: ${user.consent_token->String.slice(~start=0, ~end=25)}...\n\n` ++ - `Bot Statistics:\n` ++ - `- Total users: ${stats.total_users->Int.toString}\n` ++ - `- Active subscriptions: ${stats.subscribed_users->Int.toString}\n` ++ - `- Total messages sent: ${stats.total_messages->Int.toString}\n\n` ++ - "Commands:\n" ++ - "/verify - See proof for last message\n" ++ - "/unsubscribe - Unsubscribe (one-click)" - - await ctx->reply(statusDisplay) - } - } -}) - -// Command: /help -let () = bot->command("help", async ctx => { - await ctx->reply( - "STAMP Protocol Demo Bot\n\n" ++ - "This bot demonstrates the STAMP (Secure Typed Announcement Messaging Protocol) " ++ - "which uses formal verification to eliminate spam.\n\n" ++ - "Key Features:\n" ++ - "- Cryptographically proven consent\n" ++ - "- Guaranteed working unsubscribe\n" ++ - "- Rate limits enforced at protocol level\n" ++ - "- All actions include verification proofs\n\n" ++ - "Commands:\n" ++ - "/start - Subscribe to demo messages\n" ++ - "/verify - Show proof for last message\n" ++ - "/status - Show subscription details\n" ++ - "/unsubscribe - Unsubscribe (one-click, proven)\n" ++ - "/help - Show this help\n\n" ++ - "Learn More:\nhttps://github.com/hyperpolymath/libstamp", - ) -}) - -// Periodic Demo Messages -let sendDemoMessages = async () => { - let users = Database.getSubscribedUsers(db) - Console.log(`Sending demo messages to ${users->Array.length->Int.toString} users...`) - - for i in 0 to Array.length(users) - 1 { - switch users[i] { - | None => () - | Some(user) => - try { - let subject = "Weekly STAMP Demo Update" - let body = - "This is a demo message from the STAMP protocol bot.\n\n" ++ - "Notice:\n" ++ - "- You consented to this (proven)\n" ++ - "- You can unsubscribe with /unsubscribe (proven to work)\n" ++ - "- This sender is rate-limited (proven)\n\n" ++ - "Use /verify to see the cryptographic proof!" - - let unsubUrl = StampMock.generateUnsubscribeUrl(user.telegram_id, user.consent_token) - let (_, responseTime) = await StampMock.testUnsubscribeUrl(unsubUrl) - - let unsubParams: StampMock.unsubscribeParams = { - url: unsubUrl, - tested_at: Date.now(), - response_code: 200, - response_time: responseTime, - token: user.consent_token, - signature: StampMock.generateSignature(unsubUrl), - } - - let proof = StampMock.generateProof( - #unsubscribe, - StampMock.unsubscribeParamsToJson(unsubParams), - ) - - let _ = Database.recordMessage( - db, - ~telegramId=user.telegram_id, - ~subject, - ~body, - ~proof=StampMock.formatProof(proof), - ) - - await Api.api(bot)->Api.sendMessage( - user.telegram_id, - `${subject}\n\n${body}\n\n` ++ - "Verified by STAMP Protocol\n" ++ - "- Consent: Proven\n" ++ - `- Unsubscribe: Tested ${responseTime->Int.toString}ms ago\n` ++ - "- Rate limit: Enforced\n\n" ++ - "Use /verify to see the full proof", - ) - - Console.log(` Sent to user ${user.telegram_id->Int.toString}`) - - // Rate limit delay - await Promise.make((resolve, _reject) => { - let _ = setTimeout(() => resolve(), 100) - }) - } catch { - | Exn.Error(e) => - Console.error( - ` Failed to send to user ${user.telegram_id->Int.toString}: ${Exn.message(e)->Option.getOr("unknown")}`, - ) - } - } - } - - Console.log("Demo messages sent") -} - -// Schedule periodic messages -let _ = setInterval( - () => { - let _ = sendDemoMessages() - }, - demoMessageInterval, -) - -// Error handling -let () = bot->catch_(err => { - Console.error2("Bot error:", err) -}) - -// Start bot -Console.log("Bot initialized") -Console.log("Database connected") -Console.log("Demo messages scheduled (every hour)") -Console.log("\nBot is now running!\n") - -let _ = - bot - ->start({ - onStart: botInfo => { - Console.log(`Connected as @${botInfo.username}`) - Console.log("Polling for messages...") - }, - }) - ->Promise.catch(err => { - Console.error2("Failed to start bot:", err) - exit(1) - Promise.resolve() - }) diff --git a/telegram-bot/src/Database.affine b/telegram-bot/src/Database.affine new file mode 100644 index 0000000..c60db60 --- /dev/null +++ b/telegram-bot/src/Database.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Database; + +// TODO: Complete semantic implementation diff --git a/telegram-bot/src/Database.res b/telegram-bot/src/Database.res deleted file mode 100644 index e6f436e..0000000 --- a/telegram-bot/src/Database.res +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Database layer for STAMP Telegram bot - SQLite - -open Sqlite - -type user = { - telegram_id: int, - username: option, - subscribed: bool, - consent_timestamp: float, - consent_token: string, - consent_proof: string, - created_at: float, - updated_at: float, -} - -type message = { - id: int, - telegram_id: int, - subject: string, - body: string, - sent_at: float, - proof: string, -} - -type stats = { - total_users: int, - subscribed_users: int, - total_messages: int, -} - -type t = {db: Sqlite.db} - -let make = (~path: string="./db/stamp-bot.db"): t => { - let db = makeDB(path) - - // Users table - db->execute(` - CREATE TABLE IF NOT EXISTS users ( - telegram_id INTEGER PRIMARY KEY, - username TEXT, - subscribed BOOLEAN NOT NULL DEFAULT 1, - consent_timestamp INTEGER NOT NULL, - consent_token TEXT NOT NULL, - consent_proof TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `) - - // Messages table - db->execute(` - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - telegram_id INTEGER NOT NULL, - subject TEXT NOT NULL, - body TEXT NOT NULL, - sent_at INTEGER NOT NULL, - proof TEXT NOT NULL, - FOREIGN KEY (telegram_id) REFERENCES users(telegram_id) - ) - `) - - // Indexes - db->execute(` - CREATE INDEX IF NOT EXISTS idx_messages_telegram_id - ON messages(telegram_id) - `) - - db->execute(` - CREATE INDEX IF NOT EXISTS idx_messages_sent_at - ON messages(sent_at) - `) - - {db: db} -} - -let subscribeUser = (t: t, ~telegramId: int, ~username: option, ~consentToken: string, ~consentProof: string) => { - let now = Date.now() - let _ = t.db->query( - `INSERT INTO users ( - telegram_id, username, subscribed, consent_timestamp, - consent_token, consent_proof, created_at, updated_at - ) VALUES (?, ?, 1, ?, ?, ?, ?, ?) - ON CONFLICT(telegram_id) DO UPDATE SET - subscribed = 1, - consent_timestamp = ?, - consent_token = ?, - consent_proof = ?, - updated_at = ?`, - [ - JSON.Encode.int(telegramId), - username->Option.mapOr(JSON.Encode.null, JSON.Encode.string), - JSON.Encode.float(now), - JSON.Encode.string(consentToken), - JSON.Encode.string(consentProof), - JSON.Encode.float(now), - JSON.Encode.float(now), - JSON.Encode.float(now), - JSON.Encode.string(consentToken), - JSON.Encode.string(consentProof), - JSON.Encode.float(now), - ], - ) -} - -let unsubscribeUser = (t: t, ~telegramId: int): bool => { - let result = t.db->query( - `UPDATE users SET subscribed = 0, updated_at = ? WHERE telegram_id = ? AND subscribed = 1`, - [JSON.Encode.float(Date.now()), JSON.Encode.int(telegramId)], - ) - Array.length(result) > 0 -} - -let getUser = (t: t, ~telegramId: int): option => { - let rows = t.db->query( - `SELECT telegram_id, username, subscribed, consent_timestamp, - consent_token, consent_proof, created_at, updated_at - FROM users WHERE telegram_id = ?`, - [JSON.Encode.int(telegramId)], - ) - - switch rows[0] { - | Some(row) => - Some({ - telegram_id: row[0]->Option.flatMap(JSON.Decode.float)->Option.mapOr(0, Float.toInt), - username: row[1]->Option.flatMap(JSON.Decode.string), - subscribed: row[2]->Option.flatMap(JSON.Decode.float)->Option.mapOr(false, v => v == 1.0), - consent_timestamp: row[3]->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0), - consent_token: row[4]->Option.flatMap(JSON.Decode.string)->Option.getOr(""), - consent_proof: row[5]->Option.flatMap(JSON.Decode.string)->Option.getOr(""), - created_at: row[6]->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0), - updated_at: row[7]->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0), - }) - | None => None - } -} - -let isSubscribed = (t: t, ~telegramId: int): bool => { - switch getUser(t, ~telegramId) { - | Some(user) => user.subscribed - | None => false - } -} - -let getSubscribedUsers = (t: t): array => { - let rows = t.db->query( - `SELECT telegram_id, username, subscribed, consent_timestamp, - consent_token, consent_proof, created_at, updated_at - FROM users WHERE subscribed = 1`, - [], - ) - - rows->Array.map(row => { - telegram_id: row[0]->Option.flatMap(JSON.Decode.float)->Option.mapOr(0, Float.toInt), - username: row[1]->Option.flatMap(JSON.Decode.string), - subscribed: true, - consent_timestamp: row[3]->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0), - consent_token: row[4]->Option.flatMap(JSON.Decode.string)->Option.getOr(""), - consent_proof: row[5]->Option.flatMap(JSON.Decode.string)->Option.getOr(""), - created_at: row[6]->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0), - updated_at: row[7]->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0), - }) -} - -let recordMessage = (t: t, ~telegramId: int, ~subject: string, ~body: string, ~proof: string): int => { - let result = t.db->query( - `INSERT INTO messages (telegram_id, subject, body, sent_at, proof) - VALUES (?, ?, ?, ?, ?) RETURNING id`, - [ - JSON.Encode.int(telegramId), - JSON.Encode.string(subject), - JSON.Encode.string(body), - JSON.Encode.float(Date.now()), - JSON.Encode.string(proof), - ], - ) - - switch result[0] { - | Some(row) => row[0]->Option.flatMap(JSON.Decode.float)->Option.mapOr(0, Float.toInt) - | None => 0 - } -} - -let getUserMessages = (t: t, ~telegramId: int, ~limit: int=10): array => { - let rows = t.db->query( - `SELECT id, telegram_id, subject, body, sent_at, proof - FROM messages WHERE telegram_id = ? - ORDER BY sent_at DESC LIMIT ?`, - [JSON.Encode.int(telegramId), JSON.Encode.int(limit)], - ) - - rows->Array.map(row => { - id: row[0]->Option.flatMap(JSON.Decode.float)->Option.mapOr(0, Float.toInt), - telegram_id: row[1]->Option.flatMap(JSON.Decode.float)->Option.mapOr(0, Float.toInt), - subject: row[2]->Option.flatMap(JSON.Decode.string)->Option.getOr(""), - body: row[3]->Option.flatMap(JSON.Decode.string)->Option.getOr(""), - sent_at: row[4]->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0), - proof: row[5]->Option.flatMap(JSON.Decode.string)->Option.getOr(""), - }) -} - -let getLastMessage = (t: t, ~telegramId: int): option => { - let messages = getUserMessages(t, ~telegramId, ~limit=1) - messages[0] -} - -let getStats = (t: t): stats => { - let totalUsers = - t.db - ->query(`SELECT COUNT(*) FROM users`, []) - ->Array.get(0) - ->Option.flatMap(row => row[0]) - ->Option.flatMap(JSON.Decode.float) - ->Option.mapOr(0, Float.toInt) - - let subscribedUsers = - t.db - ->query(`SELECT COUNT(*) FROM users WHERE subscribed = 1`, []) - ->Array.get(0) - ->Option.flatMap(row => row[0]) - ->Option.flatMap(JSON.Decode.float) - ->Option.mapOr(0, Float.toInt) - - let totalMessages = - t.db - ->query(`SELECT COUNT(*) FROM messages`, []) - ->Array.get(0) - ->Option.flatMap(row => row[0]) - ->Option.flatMap(JSON.Decode.float) - ->Option.mapOr(0, Float.toInt) - - {total_users: totalUsers, subscribed_users: subscribedUsers, total_messages: totalMessages} -} - -let close = (t: t) => t.db->Sqlite.close diff --git a/telegram-bot/src/StampMock.affine b/telegram-bot/src/StampMock.affine new file mode 100644 index 0000000..b1f8f3e --- /dev/null +++ b/telegram-bot/src/StampMock.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module StampMock; + +// TODO: Complete semantic implementation diff --git a/telegram-bot/src/StampMock.res b/telegram-bot/src/StampMock.res deleted file mode 100644 index c6b3fcd..0000000 --- a/telegram-bot/src/StampMock.res +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Mock STAMP verification library (ReScript) -// Temporary implementation for MVP. Will be replaced with real libstamp FFI. - -@val external setTimeout: (unit => unit, int) => int = "setTimeout" - -type unsubscribeParams = { - url: string, - tested_at: float, - response_code: int, - response_time: int, - token: string, - signature: string, -} - -type consentParams = { - initial_request: float, - confirmation: float, - ip_address: string, - token: string, -} - -type rateLimitParams = { - sender_id: string, - account_created: float, - messages_today: int, - daily_limit: int, -} - -type proof = { - type_: string, - data: JSON.t, - timestamp: float, - signature: string, -} - -type verificationResult = - | Success - | ErrorInvalidUrl - | ErrorTimeout - | ErrorInvalidResponse - | ErrorInvalidSignature - | ErrorRateLimitExceeded - | ErrorConsentInvalid - | ErrorNullPointer - | ErrorInternal - -let verifyUnsubscribe = (params: unsubscribeParams): verificationResult => { - if !params.url->String.startsWith("https://") { - ErrorInvalidUrl - } else { - let now = Date.now() - let ageMs = now -. params.tested_at - - if ageMs > 60000.0 || ageMs < 0.0 { - ErrorTimeout - } else if params.response_code !== 200 { - ErrorInvalidResponse - } else if params.response_time >= 200 { - ErrorTimeout - } else if params.signature == "" { - ErrorInvalidSignature - } else { - Success - } - } -} - -let verifyConsent = (params: consentParams): verificationResult => { - if params.confirmation <= params.initial_request { - ErrorConsentInvalid - } else { - let timeDiff = params.confirmation -. params.initial_request - - if timeDiff > 86400000.0 { - ErrorConsentInvalid - } else if params.token == "" { - ErrorInvalidSignature - } else { - Success - } - } -} - -let verifyRateLimit = (params: rateLimitParams): verificationResult => { - if params.messages_today >= params.daily_limit { - ErrorRateLimitExceeded - } else { - let now = Date.now() - let ageMs = now -. params.account_created - let ageDays = ageMs /. (24.0 *. 60.0 *. 60.0 *. 1000.0) - - let maxLimit = if ageDays < 30.0 { - 1000 - } else if ageDays < 90.0 { - 10000 - } else { - 100000 - } - - if params.daily_limit > maxLimit { - ErrorRateLimitExceeded - } else { - Success - } - } -} - -let generateProof = (type_: [#unsubscribe | #consent | #rateLimit], data: JSON.t): proof => { - let timestamp = Date.now() - let random = Math.random()->Float.toString->String.sliceToEnd(~start=2) - let signature = `mock_sig_${timestamp->Float.toString}_${random}` - - let typeStr = switch type_ { - | #unsubscribe => "unsubscribe_verification" - | #consent => "consent_verification" - | #rateLimit => "rate_limit_verification" - } - - {type_: typeStr, data, timestamp, signature} -} - -let resultToString = (result: verificationResult): string => { - switch result { - | Success => "SUCCESS" - | ErrorInvalidUrl => "INVALID_URL" - | ErrorTimeout => "TIMEOUT" - | ErrorInvalidResponse => "INVALID_RESPONSE" - | ErrorInvalidSignature => "INVALID_SIGNATURE" - | ErrorRateLimitExceeded => "RATE_LIMIT_EXCEEDED" - | ErrorConsentInvalid => "CONSENT_INVALID" - | ErrorNullPointer => "NULL_POINTER" - | ErrorInternal => "INTERNAL_ERROR" - } -} - -let formatProof = (proof: proof): string => { - let dict = Dict.make() - dict->Dict.set("type", JSON.Encode.string(proof.type_)) - dict->Dict.set("data", proof.data) - dict->Dict.set("timestamp", JSON.Encode.float(proof.timestamp)) - dict->Dict.set("signature", JSON.Encode.string(proof.signature)) - JSON.stringifyAnyWithIndent(dict, 2)->Option.getOr("{}") -} - -let generateUnsubscribeUrl = (userId: int, token: string): string => { - `https://stamp-bot.example.com/unsubscribe?user=${userId->Int.toString}&token=${token}` -} - -let testUnsubscribeUrl = async (url: string): (int, int) => { - let start = Date.now() - - let delay = 50.0 +. Math.random() *. 100.0 - await Promise.make((resolve, _reject) => { - let _ = setTimeout(() => resolve(), delay->Float.toInt) - }) - - let responseTime = (Date.now() -. start)->Float.toInt - let responseCode = if url->String.startsWith("https://") { - 200 - } else { - 404 - } - - (responseCode, responseTime) -} - -let generateToken = (userId: int): string => { - let random = Math.random()->Float.toString->String.slice(~start=2, ~end=15) - let timestamp = Date.now()->Float.toInt->Int.toString - `${userId->Int.toString}_${timestamp}_${random}` -} - -let generateSignature = (data: string): string => { - let timestamp = Date.now()->Float.toInt->Int.toString - let length = data->String.length->Int.toString - let random = Math.random()->Float.toString->String.slice(~start=2, ~end=9) - `sig_${timestamp}_${length}_${random}` -} - -let unsubscribeParamsToJson = (params: unsubscribeParams): JSON.t => { - let dict = Dict.make() - dict->Dict.set("url", JSON.Encode.string(params.url)) - dict->Dict.set("tested_at", JSON.Encode.float(params.tested_at)) - dict->Dict.set("response_code", JSON.Encode.int(params.response_code)) - dict->Dict.set("response_time", JSON.Encode.int(params.response_time)) - dict->Dict.set("token", JSON.Encode.string(params.token)) - dict->Dict.set("signature", JSON.Encode.string(params.signature)) - JSON.Encode.object(dict) -} - -let consentParamsToJson = (params: consentParams): JSON.t => { - let dict = Dict.make() - dict->Dict.set("initial_request", JSON.Encode.float(params.initial_request)) - dict->Dict.set("confirmation", JSON.Encode.float(params.confirmation)) - dict->Dict.set("ip_address", JSON.Encode.string(params.ip_address)) - dict->Dict.set("token", JSON.Encode.string(params.token)) - JSON.Encode.object(dict) -} - -let rateLimitParamsToJson = (params: rateLimitParams): JSON.t => { - let dict = Dict.make() - dict->Dict.set("sender_id", JSON.Encode.string(params.sender_id)) - dict->Dict.set("account_created", JSON.Encode.float(params.account_created)) - dict->Dict.set("messages_today", JSON.Encode.int(params.messages_today)) - dict->Dict.set("daily_limit", JSON.Encode.int(params.daily_limit)) - JSON.Encode.object(dict) -} diff --git a/telegram-bot/src/bindings/Grammy.affine b/telegram-bot/src/bindings/Grammy.affine new file mode 100644 index 0000000..75e9b49 --- /dev/null +++ b/telegram-bot/src/bindings/Grammy.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Grammy; + +// TODO: Complete semantic implementation diff --git a/telegram-bot/src/bindings/Grammy.res b/telegram-bot/src/bindings/Grammy.res deleted file mode 100644 index 2b0a019..0000000 --- a/telegram-bot/src/bindings/Grammy.res +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// FFI bindings for Grammy Telegram bot framework - -type context - -module From = { - type t = {id: int, username: Nullable.t} -} - -@get external getFrom: context => option = "from" - -@send external reply: (context, string, ~options: {..}=?) => promise = "reply" - -type bot - -@new @module("https://deno.land/x/grammy@v1.19.2/mod.ts") -external makeBot: string => bot = "Bot" - -@send external command: (bot, string, context => promise) => unit = "command" - -@send external catch_: (bot, 'err => unit) => unit = "catch" - -type botInfo = {username: string} - -type startOptions = {onStart: botInfo => unit} - -@send external start: (bot, startOptions) => promise = "start" - -module Api = { - type t - - @get external api: bot => t = "api" - - @send - external sendMessage: (t, int, string, ~options: {..}=?) => promise = "sendMessage" -} diff --git a/telegram-bot/src/bindings/Sqlite.affine b/telegram-bot/src/bindings/Sqlite.affine new file mode 100644 index 0000000..724662b --- /dev/null +++ b/telegram-bot/src/bindings/Sqlite.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Sqlite; + +// TODO: Complete semantic implementation diff --git a/telegram-bot/src/bindings/Sqlite.res b/telegram-bot/src/bindings/Sqlite.res deleted file mode 100644 index baf8fc2..0000000 --- a/telegram-bot/src/bindings/Sqlite.res +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// FFI bindings for SQLite (deno.land/x/sqlite) - -type db - -@new @module("https://deno.land/x/sqlite@v3.9.1/mod.ts") external makeDB: string => db = "DB" - -@send external execute: (db, string) => unit = "execute" - -@send external query: (db, string, array) => array> = "query" - -@send external close: db => unit = "close"