From 281fec3fbb6c0c75f2d7eb17a4311b6f295f7969 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 22 Aug 2025 16:35:48 -0700 Subject: [PATCH 1/3] microapp poc --- frontend/layout/tests/model.ts | 1 + package.json | 3 +- waveapps/jwt/describe.json | 50 ++++ waveapps/jwt/index.html | 12 + waveapps/jwt/package.json | 39 +++ waveapps/jwt/server.js | 120 ++++++++++ waveapps/jwt/src/App.tsx | 120 ++++++++++ waveapps/jwt/src/index.css | 62 +++++ waveapps/jwt/src/main.tsx | 10 + waveapps/jwt/tsconfig.json | 25 ++ waveapps/jwt/tsconfig.node.json | 10 + waveapps/jwt/vite.config.ts | 19 ++ yarn.lock | 405 ++++++++++++++++++++++++++++++-- 13 files changed, 854 insertions(+), 22 deletions(-) create mode 100644 waveapps/jwt/describe.json create mode 100644 waveapps/jwt/index.html create mode 100644 waveapps/jwt/package.json create mode 100644 waveapps/jwt/server.js create mode 100644 waveapps/jwt/src/App.tsx create mode 100644 waveapps/jwt/src/index.css create mode 100644 waveapps/jwt/src/main.tsx create mode 100644 waveapps/jwt/tsconfig.json create mode 100644 waveapps/jwt/tsconfig.node.json create mode 100644 waveapps/jwt/vite.config.ts diff --git a/frontend/layout/tests/model.ts b/frontend/layout/tests/model.ts index 01b043fd4b..fd67c9fae2 100644 --- a/frontend/layout/tests/model.ts +++ b/frontend/layout/tests/model.ts @@ -7,5 +7,6 @@ export function newLayoutTreeState(rootNode: LayoutNode): LayoutTreeState { return { rootNode, generation: 0, + pendingBackendActions: [], }; } diff --git a/package.json b/package.json index b9ff6e2c44..64409412d4 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ }, "packageManager": "yarn@4.6.0", "workspaces": [ - "docs" + "docs", + "waveapps/*" ] } diff --git a/waveapps/jwt/describe.json b/waveapps/jwt/describe.json new file mode 100644 index 0000000000..d590272828 --- /dev/null +++ b/waveapps/jwt/describe.json @@ -0,0 +1,50 @@ +{ + "name": "JWT Decoder", + "version": "1.0.0", + "baseurl": "/", + "description": "A simple JWT token decoder that parses and displays JWT header and payload information", + "actions": [ + { + "name": "decode", + "method": "POST", + "path": "/api/decode", + "description": "Decode a JWT token and return header and payload", + "inputschema": "DecodeRequest", + "outputschema": "DecodeResponse" + } + ], + "schemas": { + "config": { + "type": "object", + "description": "Configuration for JWT decoder (currently empty)", + "properties": {} + }, + "data": { + "type": "object", + "description": "Last decoded JWT token data or error", + "properties": { + "header": { "type": "object", "description": "JWT header" }, + "payload": { "type": "object", "description": "JWT payload" }, + "signature": { "type": "string", "description": "JWT signature (base64url encoded)" }, + "error": { "type": "string", "description": "Error message if decoding failed" } + } + }, + "DecodeRequest": { + "type": "object", + "description": "Request to decode a JWT token", + "properties": { + "token": { "type": "string", "description": "JWT token to decode" } + }, + "required": ["token"] + }, + "DecodeResponse": { + "type": "object", + "description": "Decoded JWT token response", + "properties": { + "header": { "type": "object", "description": "JWT header" }, + "payload": { "type": "object", "description": "JWT payload" }, + "signature": { "type": "string", "description": "JWT signature (base64url encoded)" } + } + } + } +} \ No newline at end of file diff --git a/waveapps/jwt/index.html b/waveapps/jwt/index.html new file mode 100644 index 0000000000..6979e65070 --- /dev/null +++ b/waveapps/jwt/index.html @@ -0,0 +1,12 @@ + + + + + + JWT Decoder + + +
+ + + \ No newline at end of file diff --git a/waveapps/jwt/package.json b/waveapps/jwt/package.json new file mode 100644 index 0000000000..fe13f454d9 --- /dev/null +++ b/waveapps/jwt/package.json @@ -0,0 +1,39 @@ +{ + "name": "jwt-decoder", + "version": "1.0.0", + "description": "Simple JWT decoder waveapp", + "type": "module", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "concurrently \"node server.js\" \"vite\"", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@hono/node-server": "^1.19.0", + "hono": "^4.0.0", + "jsonwebtoken": "^9.0.2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.12", + "@types/jsonwebtoken": "^9", + "@types/node": "^20.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.0.0", + "concurrently": "^8.2.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^7.0.0" + }, + "keywords": [ + "jwt", + "decoder", + "waveapp" + ], + "author": "Wave Terminal", + "license": "Apache-2.0" +} diff --git a/waveapps/jwt/server.js b/waveapps/jwt/server.js new file mode 100644 index 0000000000..ecdc9032ff --- /dev/null +++ b/waveapps/jwt/server.js @@ -0,0 +1,120 @@ +import { serve } from "@hono/node-server"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import jwt from "jsonwebtoken"; + +const app = new Hono(); + +// Helper function to send messages to parent process +function sendWaveAppMessage(message) { + if (process.send) { + process.send(message); + } else { + console.log(`#waveapp${JSON.stringify(message)}`); + } +} + +app.use("/*", cors()); +app.use("/static/*", serveStatic({ root: "./dist" })); + +// State management +let appData = null; +let appConfig = {}; + +// Waveapp spec handlers +app.get("/config", (c) => { + return c.json(appConfig); +}); + +app.put("/config", async (c) => { + try { + const newConfig = await c.req.json(); + appConfig = { ...appConfig, ...newConfig }; + return c.json(appConfig); + } catch (error) { + return c.json({ error: "Invalid config format" }, 400); + } +}); + +app.get("/data", (c) => { + return c.json(appData || {}); +}); + +app.get("/describe", serveStatic({ path: "./describe.json" })); + +// JWT decode endpoint +app.post("/api/decode", async (c) => { + console.log("JWT decode endpoint hit"); + try { + const { token } = await c.req.json(); + console.log("Received token:", token ? "present" : "missing"); + + if (!token) { + console.log("No token provided"); + const errorData = { error: "Token is required" }; + appData = errorData; + return c.json(errorData, 400); + } + + // Try to verify to get detailed parsing errors from the library + console.log("Attempting JWT verify with dummy secret"); + try { + jwt.verify(token, "dummy-secret", { ignoreExpiration: true, ignoreNotBefore: true }); + console.log("JWT verify succeeded (unexpected)"); + } catch (verifyError) { + console.log("JWT verify error:", verifyError.name, verifyError.message); + // If it's a signature error, that's expected - continue to decode + if (verifyError.name === "JsonWebTokenError" && verifyError.message === "invalid signature") { + console.log("Expected signature error, continuing to decode"); + // Token is structurally valid, just can't verify signature + } else { + console.log("Real parsing error, returning error to client"); + // This is a real parsing/format error from the library + const errorData = { error: verifyError.message }; + appData = errorData; + return c.json(errorData, 400); + } + } + + // Decode JWT since we know it's structurally valid + console.log("Attempting JWT decode"); + const decoded = jwt.decode(token, { complete: true }); + console.log("JWT decode result:", decoded ? "success" : "null"); + + const result = { + header: decoded.header, + payload: decoded.payload, + signature: decoded.signature, + }; + + console.log("Storing result and returning"); + // Store the decoded data + appData = result; + + return c.json(result); + } catch (error) { + console.error("JWT decode error:", error); + const errorData = { error: error.message || "Failed to decode JWT token" }; + appData = errorData; + return c.json(errorData, 400); + } +}); + +// Serve static files from dist directory (built by Vite) +app.use("/*", serveStatic({ root: "./dist" })); + +const server = serve({ + fetch: app.fetch, + port: 3000, // Fixed port to match Vite proxy config +}); + +server.on("listening", () => { + const port = server.address().port; + console.log(`JWT Decoder server running on port ${port}`); + + sendWaveAppMessage({ + type: "listening", + port: port, + }); +}); diff --git a/waveapps/jwt/src/App.tsx b/waveapps/jwt/src/App.tsx new file mode 100644 index 0000000000..903445af9a --- /dev/null +++ b/waveapps/jwt/src/App.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; + +interface DecodedJWT { + header: Record; + payload: Record; + signature: string; +} + +const App: React.FC = () => { + const [token, setToken] = useState(''); + const [decoded, setDecoded] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const decodeToken = async () => { + if (!token.trim()) { + setError('Please enter a JWT token'); + setDecoded(null); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await fetch('https://github.com/api/decode', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }) + }); + + const result = await response.json(); + + if (!response.ok) { + setError(result.error || `Server error: ${response.status} ${response.statusText}`); + setDecoded(null); + return; + } + + setDecoded(result); + setError(null); + } catch (err) { + if (err instanceof TypeError && err.message.includes('fetch')) { + setError('Network error: Unable to connect to server'); + } else if (err instanceof SyntaxError) { + setError('Server response error: Invalid JSON received'); + } else { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + setError(`Failed to decode token: ${errorMessage}`); + } + setDecoded(null); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

JWT Decoder

+ +
+ {/* Input Section */} +
+
+ +