Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: schema validation #18

58 changes: 58 additions & 0 deletions .github/workflows/update-configuration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: "Update Configuration"

on:
workflow_dispatch:
push:

jobs:
update:
name: "Update Configuration in manifest.json"
runs-on: ubuntu-latest
permissions: write-all

steps:
- uses: actions/checkout@v4

- name: Setup node
uses: actions/setup-node@v4
with:
node-version: "20.10.0"

- name: Install deps and run configuration update
run: |
yarn install --immutable --immutable-cache --check-cache
yarn tsc --noCheck --project tsconfig.json

- name: Update manifest configuration using GitHub Script
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');

const { pluginSettingsSchema } = require('./src/types');

const manifestPath = path.resolve("${{ github.workspace }}", './manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));

const configuration = JSON.stringify(pluginSettingsSchema);

manifest["configuration"] = JSON.parse(configuration);

const updatedManifest = JSON.stringify(manifest, null, 2)
console.log('Updated manifest:', updatedManifest);
fs.writeFileSync(manifestPath, updatedManifest);

- name: Commit and Push generated types
run: |
git config --global user.name 'ubiquity-os[bot]'
git config --global user.email 'ubiquity-os[bot]@users.noreply.github.com'
git add ./manifest.json
if [ -n "$(git diff-index --cached --name-only HEAD)" ]; then
git commit -m "chore: updated generated configuration" || echo "Lint-staged check failed"
git push origin HEAD:${{ github.ref_name }}
else
echo "No changes to commit"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
id: wrangler_deploy
uses: cloudflare/wrangler-action@v3
with:
wranglerVersion: "3.61.0"
wranglerVersion: "3.79.0"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ Allows users to register their wallets to collect rewards.
## Example configuration

```yml
- plugin: ubiquibot/command-wallet
name: command-wallet
- plugin: https://ubiquibot-command-wallet.ubiquity.workers.dev
id: command-wallet
description: "Allows users to register their wallets to collect rewards."
command: "/wallet"
example: "/wallet ubiquibot.eth"
with:
registerWalletWithVerification: false
```

###### At this stage, your plugin will fire on your defined events with the required settings passed in from the kernel. You can now start writing your plugin's logic.
Expand Down
18 changes: 16 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
{
"name": "Wallet",
"description": "Allows users to register their wallets to collect rewards.",
"ubiquity:listeners": [ "issue_comment.created" ],
"ubiquity:listeners": [
"issue_comment.created"
],
"commands": {
"wallet": {
"ubiquity:example": "/wallet ubq.eth",
"description": "Register your wallet address for payments."
}
},
"configuration": {
"type": "object",
"properties": {
"registerWalletWithVerification": {
"default": false,
"type": "boolean"
}
},
"required": [
"registerWalletWithVerification"
]
}
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@
"ts-jest": "29.1.5",
"ts-node": "10.9.2",
"tsx": "4.15.6",
"typescript": "5.4.5",
"typescript": "5.6.2",
"typescript-eslint": "7.13.1",
"wrangler": "3.61.0"
"wrangler": "3.79.0"
},
"lint-staged": {
"*.ts": [
Expand Down
36 changes: 36 additions & 0 deletions src/handlers/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TransformDecodeCheckError, TransformDecodeError, Value, ValueError } from "@sinclair/typebox/value";
import { Env, envValidator, PluginSettings, pluginSettingsSchema, pluginSettingsValidator } from "../types";

export function validateAndDecodeSchemas(env: Env, rawSettings: object) {
const errors: ValueError[] = [];
const settings = Value.Default(pluginSettingsSchema, rawSettings) as PluginSettings;

if (!pluginSettingsValidator.test(settings)) {
for (const error of pluginSettingsValidator.errors(settings)) {
console.error(error);
errors.push(error);
}
}

if (!envValidator.test(env)) {
for (const error of envValidator.errors(env)) {
console.error(error);
errors.push(error);
}
}

if (errors.length) {
throw { errors };
}

try {
const decodedEnv = Value.Decode(envValidator.schema, env);
const decodedSettings = Value.Decode(pluginSettingsSchema, settings);
return { decodedEnv, decodedSettings };
} catch (e) {
if (e instanceof TransformDecodeCheckError || e instanceof TransformDecodeError) {
throw { errors: [e.error] };
}
throw e;
}
}
54 changes: 20 additions & 34 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Value } from "@sinclair/typebox/value";
import { plugin } from "./plugin";
import { Env, envValidator, pluginSettingsSchema, pluginSettingsValidator } from "./types";
import manifest from "../manifest.json";
import { validateAndDecodeSchemas } from "./handlers/validator";
import { plugin } from "./plugin";
import { Env } from "./types";

export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
if (request.method === "GET") {
const url = new URL(request.url);
if (url.pathname === "/manifest.json") {
const url = new URL(request.url);
if (url.pathname === "/manifest") {
if (request.method === "GET") {
return new Response(JSON.stringify(manifest), {
headers: { "content-type": "application/json" },
});
} else if (request.method === "POST") {
const webhookPayload = await request.json();
validateAndDecodeSchemas(env, webhookPayload.settings);
return new Response(JSON.stringify({ message: "Schema is valid" }), { status: 200, headers: { "content-type": "application/json" } });
}
}
if (request.method !== "POST") {
Expand All @@ -29,41 +33,23 @@ export default {
}

const webhookPayload = await request.json();
const settings = Value.Decode(pluginSettingsSchema, Value.Default(pluginSettingsSchema, webhookPayload.settings));

if (!pluginSettingsValidator.test(settings)) {
const errors: string[] = [];
for (const error of pluginSettingsValidator.errors(settings)) {
console.error(error);
errors.push(`${error.path}: ${error.message}`);
}
return new Response(JSON.stringify({ error: `Error: "Invalid settings provided. ${errors.join("; ")}"` }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
if (!envValidator.test(env)) {
const errors: string[] = [];
for (const error of envValidator.errors(env)) {
console.error(error);
errors.push(`${error.path}: ${error.message}`);
}
return new Response(JSON.stringify({ error: `Error: "Invalid environment provided. ${errors.join("; ")}"` }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
webhookPayload.settings = settings;
await plugin(webhookPayload, env);
const { decodedSettings, decodedEnv } = validateAndDecodeSchemas(env, webhookPayload.settings);

webhookPayload.settings = decodedSettings;
await plugin(webhookPayload, decodedEnv);
return new Response(JSON.stringify("OK"), { status: 200, headers: { "content-type": "application/json" } });
} catch (error) {
return handleUncaughtError(error);
}
},
};

function handleUncaughtError(error: unknown) {
console.error(error);
function handleUncaughtError(errors: unknown) {
console.error(errors);
const status = 500;
return new Response(JSON.stringify({ error }), { status: status, headers: { "content-type": "application/json" } });
return new Response(JSON.stringify(errors), {
status: status,
headers: { "content-type": "application/json" },
});
}
Loading