UNPKG

@getcronit/pylon

Version:

![Pylon cover](https://github.com/user-attachments/assets/c28e49b2-5672-4849-826e-8b2eab0360cc)

1,445 lines (1,401 loc) 65.8 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/define-pylon.ts import * as Sentry from "@sentry/bun"; import consola from "consola"; import { GraphQLError } from "graphql"; // src/context.ts import { AsyncLocalStorage } from "async_hooks"; import { sendFunctionEvent } from "@getcronit/pylon-telemetry"; import { env } from "hono/adapter"; var asyncContext = new AsyncLocalStorage(); var getContext = () => { const start = Date.now(); const ctx = asyncContext.getStore(); sendFunctionEvent({ name: "getContext", duration: Date.now() - start }).then(() => { }); if (!ctx) { throw new Error("Context not defined"); } ctx.env = env(ctx); return ctx; }; var setContext = (context) => { return asyncContext.enterWith(context); }; // src/define-pylon.ts import { isAsyncIterable } from "graphql-yoga"; function getAllPropertyNames(instance) { const allProps = /* @__PURE__ */ new Set(); let currentObj = instance; while (currentObj && currentObj !== Object.prototype) { const ownProps = Object.getOwnPropertyNames(currentObj); ownProps.forEach((prop) => allProps.add(prop)); currentObj = Object.getPrototypeOf(currentObj); } return Array.from(allProps).filter((prop) => prop !== "constructor"); } async function wrapFunctionsRecursively(obj, wrapper, that = null, selectionSet = [], info) { if (obj === null || obj instanceof Date) { return obj; } if (Array.isArray(obj)) { return await Promise.all( obj.map(async (item) => { return await wrapFunctionsRecursively( item, wrapper, that, selectionSet, info ); }) ); } else if (typeof obj === "function") { return Sentry.startSpan( { name: obj.name, op: "pylon.fn" }, async () => { return await wrapper.call(that, obj, selectionSet, info); } ); } else if (obj instanceof Promise) { return await wrapFunctionsRecursively( await obj, wrapper, that, selectionSet, info ); } else if (isAsyncIterable(obj)) { return obj; } else if (typeof obj === "object") { that = obj; const result = {}; for (const key of getAllPropertyNames(obj)) { result[key] = await wrapFunctionsRecursively( obj[key], wrapper, that, selectionSet, info ); } return result; } else { return await obj; } } function spreadFunctionArguments(fn) { return (otherArgs, c, info) => { const selections = arguments[1]; const realInfo = arguments[2]; let args = {}; if (info) { const type = info.parentType; const field = type.getFields()[info.fieldName]; const fieldArguments = field?.args; const preparedArguments = fieldArguments?.reduce( (acc, arg) => { if (otherArgs[arg.name] !== void 0) { acc[arg.name] = otherArgs[arg.name]; } else { acc[arg.name] = void 0; } return acc; }, {} ); if (preparedArguments) { args = preparedArguments; } } else { args = otherArgs; } const orderedArgs = Object.keys(args).map((key) => args[key]); const that = this || {}; const result = wrapFunctionsRecursively( fn.call(that, ...orderedArgs), spreadFunctionArguments, this, selections, realInfo ); return result; }; } var resolversToGraphQLResolvers = (resolvers, configureContext) => { const rootGraphqlResolver = (fn) => async (_, args, ctx, info) => { return Sentry.withScope(async (scope) => { const ctx2 = asyncContext.getStore(); if (!ctx2) { consola.warn( "Context is not defined. Make sure AsyncLocalStorage is supported in your environment." ); } ctx2?.set("graphqlResolveInfo", info); const auth = ctx2?.get("auth"); if (auth?.user) { scope.setUser({ id: auth.user.sub, username: auth.user.preferred_username, email: auth.user.email, details: auth.user }); } let type = null; switch (info.operation.operation) { case "query": type = info.schema.getQueryType(); break; case "mutation": type = info.schema.getMutationType(); break; case "subscription": type = info.schema.getSubscriptionType(); break; default: throw new Error("Unknown operation"); } const field = type?.getFields()[info.fieldName]; const fieldArguments = field?.args || []; const preparedArguments = fieldArguments.reduce( (acc, arg) => { if (args[arg.name] !== void 0) { acc[arg.name] = args[arg.name]; } else { acc[arg.name] = void 0; } return acc; }, {} ); let inner = await fn; let baseSelectionSet = []; for (const selection of info.operation.selectionSet.selections) { if (selection.kind === "Field" && selection.name.value === info.fieldName) { baseSelectionSet = selection.selectionSet?.selections || []; } } const wrappedFn = await wrapFunctionsRecursively( inner, spreadFunctionArguments, void 0, baseSelectionSet, info ); if (typeof wrappedFn !== "function") { return wrappedFn; } const res = await wrappedFn(preparedArguments); return res; }); }; const graphqlResolvers = {}; for (const key of Object.keys(resolvers.Query)) { if (!resolvers.Query[key]) { delete resolvers.Query[key]; } } if (resolvers.Query && Object.keys(resolvers.Query).length > 0) { for (const [key, value] of Object.entries(resolvers.Query)) { if (!graphqlResolvers.Query) { graphqlResolvers.Query = {}; } graphqlResolvers.Query[key] = rootGraphqlResolver( value ); } } if (resolvers.Mutation && Object.keys(resolvers.Mutation).length > 0) { if (!graphqlResolvers.Mutation) { graphqlResolvers.Mutation = {}; } for (const [key, value] of Object.entries(resolvers.Mutation)) { graphqlResolvers.Mutation[key] = rootGraphqlResolver( value ); } } if (resolvers.Subscription && Object.keys(resolvers.Subscription).length > 0) { if (!graphqlResolvers.Subscription) { graphqlResolvers.Subscription = {}; } for (const [key, value] of Object.entries(resolvers.Subscription)) { graphqlResolvers.Subscription[key] = { subscribe: rootGraphqlResolver(value), resolve: (payload) => payload }; } } if (!graphqlResolvers.Query) { throw new Error(`At least one 'Query' resolver must be provided. Example: export const graphql = { Query: { // Define at least one query resolver here hello: () => 'world' } } `); } for (const key of Object.keys(resolvers)) { if (key !== "Query" && key !== "Mutation" && key !== "Subscription") { graphqlResolvers[key] = resolvers[key]; } } return graphqlResolvers; }; var ServiceError = class extends GraphQLError { extensions; constructor(message, extensions, error) { super(message, { originalError: error }); this.extensions = extensions; this.cause = error; } }; // src/plugins/use-auth/use-auth.ts import { promises as fs } from "fs"; import { deleteCookie, getCookie, setCookie } from "hono/cookie"; import { HTTPException } from "hono/http-exception"; import * as openid from "openid-client"; import path from "path"; // src/plugins/use-auth/import-private-key.ts import * as crypto2 from "crypto"; function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } var convertPKCS1ToPKCS8 = (pkcs1) => { const key = crypto2.createPrivateKey(pkcs1); return key.export({ type: "pkcs8", format: "pem" }); }; function importPKCS8PrivateKey(pem) { const pemHeader = "-----BEGIN PRIVATE KEY-----"; const pemFooter = "-----END PRIVATE KEY-----"; const pemContents = pem.substring( pemHeader.length, pem.length - pemFooter.length - 1 ); const binaryDerString = atob(pemContents); const binaryDer = str2ab(binaryDerString); return crypto2.subtle.importKey( "pkcs8", binaryDer, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["sign"] ); } var importPrivateKey = async (pkcs1Pem) => { const pkcs8Pem = convertPKCS1ToPKCS8(pkcs1Pem); return await importPKCS8PrivateKey(pkcs8Pem); }; // src/plugins/use-auth/use-auth.ts var loadAuthKey = async (keyPath) => { const authKeyFilePath = path.join(process.cwd(), keyPath); const env3 = getContext().env; if (env3.AUTH_KEY) { try { return JSON.parse(env3.AUTH_KEY); } catch (error) { throw new Error( "Error while reading AUTH_KEY. Make sure it is valid JSON" ); } } try { const ketFileContent = await fs.readFile(authKeyFilePath, "utf-8"); try { return JSON.parse(ketFileContent); } catch (error) { throw new Error( "Error while reading key file. Make sure it is valid JSON" ); } } catch (error) { throw new Error("Error while reading key file. Make sure it exists"); } }; var openidConfigCache; var bootstrapAuth = async (issuer, keyPath) => { if (!openidConfigCache) { const authKey = await loadAuthKey(keyPath); openidConfigCache = await openid.discovery( new URL(issuer), authKey.clientId, void 0, openid.PrivateKeyJwt({ key: await importPrivateKey(authKey.key), kid: authKey.keyId }) ); } return openidConfigCache; }; var PylonAuthException = class extends HTTPException { // Same constructor as HTTPException constructor(...args) { args[1] = { ...args[1], message: `PylonAuthException: ${args[1]?.message}` }; super(...args); } }; function useAuth(args) { const { issuer, endpoint = "/auth", keyPath = "key.json" } = args; const loginPath = `${endpoint}/login`; const logoutPath = `${endpoint}/logout`; const callbackPath = `${endpoint}/callback`; return { middleware: async (ctx, next) => { const openidConfig = await bootstrapAuth(issuer, keyPath); ctx.set("auth", { openidConfig }); const authCookieToken = getCookie(ctx, "pylon-auth"); const authHeader = ctx.req.header("Authorization"); const authQueryToken = ctx.req.query("token"); if (authCookieToken || authHeader || authQueryToken) { let token; if (authHeader) { const [type, value] = authHeader.split(" "); if (type === "Bearer") { token = value; } } else if (authQueryToken) { token = authQueryToken; } else if (authCookieToken) { token = authCookieToken; } if (!token) { throw new PylonAuthException(401, { message: "Invalid token" }); } const introspection = await openid.tokenIntrospection( openidConfig, token, { scope: "openid email profile" } ); if (!introspection.active) { throw new PylonAuthException(401, { message: "Token is not active" }); } if (!introspection.sub) { throw new PylonAuthException(401, { message: "Token is missing subject" }); } const userInfo = await openid.fetchUserInfo( openidConfig, token, introspection.sub ); const roles = Object.keys( introspection["urn:zitadel:iam:org:projects:roles"]?.valueOf() || {} ); ctx.set("auth", { user: { ...userInfo, roles }, openidConfig }); return next(); } }, setup(app2) { app2.get(loginPath, async (ctx) => { const openidConfig = ctx.get("auth").openidConfig; const codeVerifier = openid.randomPKCECodeVerifier(); const codeChallenge = await openid.calculatePKCECodeChallenge( codeVerifier ); setCookie(ctx, "pylon_code_verifier", codeVerifier, { httpOnly: true, maxAge: 300 // 5 minutes }); let scope = "openid profile email urn:zitadel:iam:user:resourceowner urn:zitadel:iam:org:projects:roles"; const parameters = { scope, code_challenge: codeChallenge, code_challenge_method: "S256", redirect_uri: new URL(ctx.req.url).origin + "/auth/callback", state: openid.randomState() }; const authorizationUrl = openid.buildAuthorizationUrl( openidConfig, parameters ); return ctx.redirect(authorizationUrl); }); app2.get(logoutPath, async (ctx) => { deleteCookie(ctx, "pylon-auth"); return ctx.redirect("/"); }); app2.get(callbackPath, async (ctx) => { const openidConfig = ctx.get("auth").openidConfig; const params = ctx.req.query(); const code = params.code; const state = params.state; if (!code || !state) { throw new PylonAuthException(400, { message: "Missing authorization code or state" }); } const codeVerifier = getCookie(ctx, "pylon_code_verifier"); if (!codeVerifier) { throw new PylonAuthException(400, { message: "Missing code verifier" }); } try { const cbUrl = new URL(ctx.req.url); let tokenSet = await openid.authorizationCodeGrant( openidConfig, cbUrl, { pkceCodeVerifier: codeVerifier, expectedState: state }, cbUrl.searchParams ); setCookie(ctx, `pylon-auth`, tokenSet.access_token, { httpOnly: true, maxAge: tokenSet.expires_in || 3600 // Default to 1 hour if not specified }); return ctx.redirect("/"); } catch (error) { console.error("Error during token exchange:", error); return ctx.text("Authentication failed!", 500); } }); } }; } // src/plugins/use-auth/auth-require.ts import { env as env2 } from "hono/adapter"; import { HTTPException as HTTPException2 } from "hono/http-exception"; // src/create-decorator.ts import { sendFunctionEvent as sendFunctionEvent2 } from "@getcronit/pylon-telemetry"; function createDecorator(callback) { sendFunctionEvent2({ name: "createDecorator", duration: 0 }).then(() => { }); function MyDecorator(arg1, propertyKey, descriptor) { if (descriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args) { await callback(...args); return originalMethod.apply(this, args); }; return descriptor; } else { if (!descriptor) { if (propertyKey === void 0) { const originalFunction = arg1; return async function(...args) { await callback(...args); return originalFunction(...args); }; } let value = arg1[propertyKey]; Object.defineProperty(arg1, propertyKey, { get: function() { return async function(...args) { await callback(...args); if (typeof value === "function") { return value(...args); } return value; }; }, set: function(newValue) { value = newValue; }, enumerable: true, configurable: true }); return; } } } return MyDecorator; } // src/plugins/use-auth/auth-require.ts var authMiddleware = (checks = {}) => { const middleware = async (ctx, next) => { const AUTH_PROJECT_ID = env2(ctx).AUTH_PROJECT_ID; const auth = ctx.get("auth"); if (!auth) { throw new HTTPException2(401, { message: "Authentication required" }); } if (checks.roles && auth.user) { const roles = auth.user.roles; const hasRole = checks.roles.some((role) => { return roles.includes(role) || roles.includes(`${AUTH_PROJECT_ID}:${role}`); }); if (!hasRole) { const resError = new Response("Forbidden", { status: 403, statusText: "Forbidden", headers: { "Missing-Roles": checks.roles.join(","), "Obtained-Roles": roles.join(",") } }); throw new HTTPException2(resError.status, { res: resError }); } } return next(); }; return middleware; }; function requireAuth(checks) { const checkAuth = async (c) => { const ctx = await c; try { await authMiddleware(checks)(ctx, async () => { }); } catch (e) { if (e instanceof HTTPException2) { if (e.status === 401) { throw new ServiceError(e.message, { statusCode: 401, code: "AUTH_REQUIRED" }); } else if (e.status === 403) { const res = e.getResponse(); throw new ServiceError(res.statusText, { statusCode: res.status, code: "AUTHORIZATION_REQUIRED", details: { missingRoles: res.headers.get("Missing-Roles")?.split(","), obtainedRoles: res.headers.get("Obtained-Roles")?.split(",") } }); } else { throw e; } } throw e; } }; return createDecorator(async () => { const ctx = getContext(); await checkAuth(ctx); }); } // src/app/index.ts import { Hono } from "hono"; import { logger } from "hono/logger"; import { sentry } from "@hono/sentry"; import { except } from "hono/combine"; var app = new Hono(); app.use("*", sentry()); app.use("*", async (c, next) => { return new Promise((resolve, reject) => { asyncContext.run(c, async () => { try { resolve(await next()); } catch (error) { reject(error); } }); }); }); app.use("*", except(["/__pylon/static/*"], logger())); app.use((c, next) => { c.req.id = crypto.randomUUID(); return next(); }); var pluginsMiddleware = []; var pluginsMiddlewareLoader = async (c, next) => { for (const middleware of pluginsMiddleware) { const response = await middleware(c, async () => { }); if (response) { return response; } } return next(); }; app.use(pluginsMiddlewareLoader); // src/app/pylon-handler.ts import { createSchema, createYoga } from "graphql-yoga"; import { GraphQLScalarType, Kind as Kind2 } from "graphql"; import { DateTimeISOResolver, GraphQLVoid, JSONObjectResolver, JSONResolver } from "graphql-scalars"; // src/plugins/use-sentry.ts import { Kind, print } from "graphql"; import { getDocumentString, handleStreamOrSingleExecutionResult, isOriginalGraphQLError } from "@envelop/core"; import * as Sentry2 from "@sentry/node"; var defaultSkipError = isOriginalGraphQLError; var useSentry = (options = {}) => { function pick(key, defaultValue) { return options[key] ?? defaultValue; } const startTransaction = pick("startTransaction", true); const includeRawResult = pick("includeRawResult", false); const includeExecuteVariables = pick("includeExecuteVariables", false); const renameTransaction = pick("renameTransaction", false); const skipOperation = pick("skip", () => false); const skipError = pick("skipError", defaultSkipError); const eventIdKey = options.eventIdKey === null ? null : "sentryEventId"; function addEventId(err, eventId) { if (eventIdKey !== null && eventId !== null) { err.extensions[eventIdKey] = eventId; } return err; } return { onExecute({ args }) { if (skipOperation(args)) { return; } const rootOperation = args.document.definitions.find( (o) => o.kind === Kind.OPERATION_DEFINITION ); const operationType = rootOperation.operation; const document = getDocumentString(args.document, print); const opName = args.operationName || rootOperation.name?.value || "Anonymous Operation"; const addedTags = options.appendTags && options.appendTags(args) || {}; const traceparentData = options.traceparentData && options.traceparentData(args) || {}; const transactionName = options.transactionName ? options.transactionName(args) : opName; const op = options.operationName ? options.operationName(args) : "execute"; const tags = { operationName: opName, operation: operationType, ...addedTags }; if (options.configureScope) { options.configureScope(args, Sentry2.getCurrentScope()); } return { onExecuteDone(payload) { const handleResult = ({ result, setResult }) => { Sentry2.startSpanManual( { op, name: opName, attributes: tags }, (span) => { if (renameTransaction) { span.updateName(transactionName); } span.setAttribute("document", document); if (includeRawResult) { span.setAttribute("result", JSON.stringify(result)); } if (result.errors && result.errors.length > 0) { Sentry2.withScope((scope) => { scope.setTransactionName(opName); scope.setTag("operation", operationType); scope.setTag("operationName", opName); scope.setExtra("document", document); scope.setTags(addedTags || {}); if (includeRawResult) { scope.setExtra("result", result); } if (includeExecuteVariables) { scope.setExtra("variables", args.variableValues); } const errors = result.errors?.map((err) => { if (skipError(err) === true) { return err; } const errorPath = (err.path ?? []).map( (v) => typeof v === "number" ? "$index" : v ).join(" > "); if (errorPath) { scope.addBreadcrumb({ category: "execution-path", message: errorPath, level: "debug" }); } const eventId = Sentry2.captureException( err.originalError, { fingerprint: [ "graphql", errorPath, opName, operationType ], contexts: { GraphQL: { operationName: opName, operationType, variables: args.variableValues } } } ); return addEventId(err, eventId); }); setResult({ ...result, errors }); }); } span.end(); } ); }; return handleStreamOrSingleExecutionResult(payload, handleResult); } }; } }; }; // src/app/pylon-handler.ts import { readFileSync } from "fs"; import path2 from "path"; // src/plugins/use-viewer.ts import { html } from "hono/html"; function useViewer() { return { onRequest: async ({ request, fetchAPI, endResponse, url }) => { const c = getContext(); if (request.method === "GET" && url.pathname === "/viewer") { endResponse( c.html( await html` <!DOCTYPE html> <html> <head> <title>Pylon Viewer</title> <script src="https://cdn.jsdelivr.net/npm/react@16/umd/react.production.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.production.min.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-voyager/dist/voyager.css" /> <style> body { padding: 0; margin: 0; width: 100%; height: 100vh; overflow: hidden; } #voyager { height: 100%; position: relative; } } </style> <script src="https://cdn.jsdelivr.net/npm/graphql-voyager/dist/voyager.min.js"></script> </head> <body> <div id="voyager">Loading...</div> <script> function introspectionProvider(introspectionQuery) { // ... do a call to server using introspectionQuery provided // or just return pre-fetched introspection // Endpoint is current path instead of root/graphql const endpoint = window.location.pathname.replace( '/viewer', '/graphql' ) return fetch(endpoint, { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({query: introspectionQuery}) }).then(response => response.json()) } // Render <Voyager /> GraphQLVoyager.init(document.getElementById('voyager'), { introspection: introspectionProvider }) </script> </body> </html> ` ) ); } } }; } // src/plugins/use-unhandled-route.ts import { getVersions } from "@getcronit/pylon-telemetry"; import { html as html2 } from "hono/html"; function useUnhandledRoute(args) { const versions = getVersions(); return { setup: (app2) => { app2.use(async (c, next) => { if (c.req.method === "GET" && c.req.path !== args.graphqlEndpoint) { return c.html( await html2`<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Welcome to Pylon</title> <link rel="icon" href="https://pylon.cronit.io/favicon/favicon.ico" /> <style> body, html { padding: 0; margin: 0; height: 100%; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: white; background-color: black; } main > section.hero { display: flex; height: 90vh; justify-content: center; align-items: center; flex-direction: column; } .logo { display: flex; align-items: center; } .logo-svg { width: 100% } .buttons { margin-top: 24px; } h1 { font-size: 80px; } h2 { color: #888; max-width: 50%; margin-top: 0; text-align: center; } a { color: #fff; text-decoration: none; margin-left: 10px; margin-right: 10px; font-weight: bold; transition: color 0.3s ease; padding: 4px; overflow: visible; } a.graphiql:hover { color: rgba(255, 0, 255, 0.7); } a.docs:hover { color: rgba(28, 200, 238, 0.7); } a.tutorial:hover { color: rgba(125, 85, 245, 0.7); } svg { margin-right: 24px; } .not-what-your-looking-for { margin-top: 5vh; } .not-what-your-looking-for > * { margin-left: auto; margin-right: auto; } .not-what-your-looking-for > p { text-align: center; } .not-what-your-looking-for > h2 { color: #464646; } .not-what-your-looking-for > p { max-width: 600px; line-height: 1.3em; } .not-what-your-looking-for > pre { max-width: 300px; } </style> </head> <body id="body"> <main> <section class="hero"> <div class="logo"> <div> <svg class="logo-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" zoomAndPan="magnify" viewBox="0 0 286.5 121.500001" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g></g><clipPath id="38f6fcde47"><path d="M 0.339844 42 L 10 42 L 10 79 L 0.339844 79 Z M 0.339844 42 " clip-rule="nonzero"></path></clipPath><clipPath id="af000f7256"><path d="M 64 23.925781 L 72.789062 23.925781 L 72.789062 96.378906 L 64 96.378906 Z M 64 23.925781 " clip-rule="nonzero"></path></clipPath></defs><g fill="currentColor" fill-opacity="1"><g transform="translate(107.11969, 78.49768)"><g><path d="M 10.078125 -25.046875 C 11.109375 -26.398438 12.507812 -27.535156 14.28125 -28.453125 C 16.0625 -29.378906 18.070312 -29.84375 20.3125 -29.84375 C 22.863281 -29.84375 25.195312 -29.210938 27.3125 -27.953125 C 29.425781 -26.691406 31.085938 -24.921875 32.296875 -22.640625 C 33.503906 -20.367188 34.109375 -17.757812 34.109375 -14.8125 C 34.109375 -11.863281 33.503906 -9.222656 32.296875 -6.890625 C 31.085938 -4.566406 29.425781 -2.753906 27.3125 -1.453125 C 25.195312 -0.160156 22.863281 0.484375 20.3125 0.484375 C 18.070312 0.484375 16.078125 0.03125 14.328125 -0.875 C 12.585938 -1.78125 11.171875 -2.910156 10.078125 -4.265625 L 10.078125 13.96875 L 4 13.96875 L 4 -29.359375 L 10.078125 -29.359375 Z M 27.921875 -14.8125 C 27.921875 -16.84375 27.503906 -18.59375 26.671875 -20.0625 C 25.835938 -21.539062 24.734375 -22.660156 23.359375 -23.421875 C 21.992188 -24.179688 20.53125 -24.5625 18.96875 -24.5625 C 17.445312 -24.5625 16 -24.171875 14.625 -23.390625 C 13.257812 -22.609375 12.160156 -21.472656 11.328125 -19.984375 C 10.492188 -18.492188 10.078125 -16.734375 10.078125 -14.703125 C 10.078125 -12.679688 10.492188 -10.914062 11.328125 -9.40625 C 12.160156 -7.894531 13.257812 -6.75 14.625 -5.96875 C 16 -5.1875 17.445312 -4.796875 18.96875 -4.796875 C 20.53125 -4.796875 21.992188 -5.191406 23.359375 -5.984375 C 24.734375 -6.785156 25.835938 -7.953125 26.671875 -9.484375 C 27.503906 -11.015625 27.921875 -12.789062 27.921875 -14.8125 Z M 27.921875 -14.8125 "></path></g></g></g><g fill="currentColor" fill-opacity="1"><g transform="translate(143.259256, 78.49768)"><g><path d="M 30.4375 -29.359375 L 12.421875 13.796875 L 6.125 13.796875 L 12.09375 -0.484375 L 0.53125 -29.359375 L 7.296875 -29.359375 L 15.5625 -6.984375 L 24.140625 -29.359375 Z M 30.4375 -29.359375 "></path></g></g></g><g fill="currentColor" fill-opacity="1"><g transform="translate(174.281707, 78.49768)"><g><path d="M 10.078125 -39.4375 L 10.078125 0 L 4 0 L 4 -39.4375 Z M 10.078125 -39.4375 "></path></g></g></g><g fill="currentColor" fill-opacity="1"><g transform="translate(188.353752, 78.49768)"><g><path d="M 16.734375 0.484375 C 13.960938 0.484375 11.457031 -0.144531 9.21875 -1.40625 C 6.976562 -2.664062 5.21875 -4.441406 3.9375 -6.734375 C 2.664062 -9.035156 2.03125 -11.691406 2.03125 -14.703125 C 2.03125 -17.691406 2.6875 -20.335938 4 -22.640625 C 5.3125 -24.953125 7.101562 -26.726562 9.375 -27.96875 C 11.65625 -29.21875 14.195312 -29.84375 17 -29.84375 C 19.8125 -29.84375 22.351562 -29.21875 24.625 -27.96875 C 26.894531 -26.726562 28.6875 -24.953125 30 -22.640625 C 31.320312 -20.335938 31.984375 -17.691406 31.984375 -14.703125 C 31.984375 -11.722656 31.304688 -9.078125 29.953125 -6.765625 C 28.597656 -4.453125 26.757812 -2.664062 24.4375 -1.40625 C 22.113281 -0.144531 19.546875 0.484375 16.734375 0.484375 Z M 16.734375 -4.796875 C 18.296875 -4.796875 19.757812 -5.164062 21.125 -5.90625 C 22.5 -6.65625 23.613281 -7.773438 24.46875 -9.265625 C 25.320312 -10.765625 25.75 -12.578125 25.75 -14.703125 C 25.75 -16.835938 25.335938 -18.640625 24.515625 -20.109375 C 23.703125 -21.585938 22.617188 -22.695312 21.265625 -23.4375 C 19.910156 -24.1875 18.453125 -24.5625 16.890625 -24.5625 C 15.328125 -24.5625 13.878906 -24.1875 12.546875 -23.4375 C 11.210938 -22.695312 10.15625 -21.585938 9.375 -20.109375 C 8.59375 -18.640625 8.203125 -16.835938 8.203125 -14.703125 C 8.203125 -11.546875 9.007812 -9.101562 10.625 -7.375 C 12.25 -5.65625 14.285156 -4.796875 16.734375 -4.796875 Z M 16.734375 -4.796875 "></path></g></g></g><g fill="currentColor" fill-opacity="1"><g transform="translate(222.361196, 78.49768)"><g><path d="M 18.8125 -29.84375 C 21.125 -29.84375 23.191406 -29.363281 25.015625 -28.40625 C 26.847656 -27.445312 28.28125 -26.023438 29.3125 -24.140625 C 30.34375 -22.253906 30.859375 -19.984375 30.859375 -17.328125 L 30.859375 0 L 24.84375 0 L 24.84375 -16.421875 C 24.84375 -19.046875 24.179688 -21.054688 22.859375 -22.453125 C 21.546875 -23.859375 19.753906 -24.5625 17.484375 -24.5625 C 15.210938 -24.5625 13.410156 -23.859375 12.078125 -22.453125 C 10.742188 -21.054688 10.078125 -19.046875 10.078125 -16.421875 L 10.078125 0 L 4 0 L 4 -29.359375 L 10.078125 -29.359375 L 10.078125 -26.015625 C 11.066406 -27.222656 12.332031 -28.160156 13.875 -28.828125 C 15.425781 -29.503906 17.070312 -29.84375 18.8125 -29.84375 Z M 18.8125 -29.84375 "></path></g></g></g><path fill="currentColor" d="M 53.359375 31.652344 L 53.359375 88.6875 L 62.410156 90.859375 L 62.410156 29.484375 Z M 53.359375 31.652344 " fill-opacity="1" fill-rule="nonzero"></path><g clip-path="url(#38f6fcde47)"><path fill="currentColor" d="M 0.339844 47.433594 L 0.339844 72.910156 C 0.339844 73.34375 0.410156 73.769531 0.554688 74.179688 C 0.699219 74.59375 0.90625 74.96875 1.175781 75.3125 C 1.445312 75.65625 1.765625 75.945312 2.132812 76.179688 C 2.503906 76.414062 2.898438 76.582031 3.324219 76.683594 L 9.390625 78.140625 L 9.390625 42.195312 L 3.3125 43.660156 C 2.890625 43.761719 2.492188 43.929688 2.125 44.164062 C 1.761719 44.402344 1.441406 44.6875 1.171875 45.03125 C 0.902344 45.375 0.695312 45.75 0.554688 46.164062 C 0.410156 46.574219 0.339844 46.996094 0.339844 47.433594 Z M 0.339844 47.433594 " fill-opacity="1" fill-rule="nonzero"></path></g><g clip-path="url(#af000f7256)"><path fill="currentColor" d="M 64.996094 95.085938 L 64.996094 25.253906 C 64.996094 25.082031 65.027344 24.917969 65.09375 24.761719 C 65.160156 24.601562 65.253906 24.460938 65.375 24.339844 C 65.496094 24.21875 65.636719 24.125 65.792969 24.0625 C 65.953125 23.996094 66.117188 23.960938 66.289062 23.960938 L 71.460938 23.960938 C 71.632812 23.960938 71.796875 23.996094 71.957031 24.0625 C 72.113281 24.125 72.253906 24.21875 72.375 24.339844 C 72.496094 24.460938 72.589844 24.601562 72.65625 24.761719 C 72.722656 24.917969 72.753906 25.082031 72.753906 25.253906 L 72.753906 95.085938 C 72.753906 95.257812 72.722656 95.421875 72.65625 95.582031 C 72.589844 95.738281 72.496094 95.878906 72.375 96 C 72.253906 96.121094 72.113281 96.214844 71.957031 96.28125 C 71.796875 96.347656 71.632812 96.378906 71.460938 96.378906 L 66.289062 96.378906 C 66.117188 96.378906 65.953125 96.347656 65.792969 96.28125 C 65.636719 96.214844 65.496094 96.121094 65.375 96 C 65.253906 95.878906 65.160156 95.738281 65.09375 95.582031 C 65.027344 95.421875 64.996094 95.257812 64.996094 95.085938 Z M 64.996094 95.085938 " fill-opacity="1" fill-rule="nonzero"></path></g><path fill="currentColor" d="M 22.320312 81.238281 L 22.320312 39.101562 L 11.976562 41.585938 L 11.976562 78.757812 Z M 22.320312 81.238281 " fill-opacity="1" fill-rule="nonzero"></path><path fill="currentColor" d="M 50.769531 88.066406 L 50.769531 32.277344 L 37.839844 35.378906 L 37.839844 84.960938 Z M 50.769531 88.066406 " fill-opacity="1" fill-rule="nonzero"></path><path fill="currentColor" d="M 24.90625 81.863281 L 35.253906 84.34375 L 35.253906 35.996094 L 24.90625 38.480469 Z M 24.90625 81.863281 " fill-opacity="1" fill-rule="nonzero"></path></svg> </div> <p>Version: ${versions.pylonVersion}</p> </div> <h2>Enables TypeScript developers to easily build GraphQL APIs</h2> <div class="buttons"> <a href="https://pylon.cronit.io/docs" class="docs" >Read the Docs</add > <a href="/graphql" class="graphiql">Visit GraphiQL</a> <a href="/viewer" class="graphiql">Visit Viewer</a> </div> </section> <section class="not-what-your-looking-for"> <h2>Not the page you are looking for? 👀</h2> <p> This page is shown be default whenever a 404 is hit.<br />You can disable this by behavior via the <code>landingPage</code> option in the Pylon config. Edit the <code>src/index.ts</code> file and add the following code: </p> <pre> <code> export const config: PylonConfig = { landingPage: false } </code> </pre> <p> When you define a route, this page will no longer be shown. For example, the following code will show a "Hello, world!" message at the root of your app: </p> <pre> <code> import {app} from '@getcronit/pylon' app.get("/", c => { return c.text("Hello, world!") }) </code> </pre> </section> </main> </body> </html>`, 404 ); } return next(); }); } }; } // src/app/pylon-handler.ts var resolveLazyObject = (obj) => { return typeof obj === "function" ? obj() : obj; }; var handler = (options) => { let { typeDefs, resolvers, graphql: graphql$, config: config$ } = options; const loadPluginsMiddleware = (plugins2) => { for (const plugin of plugins2) { plugin.setup?.(app); if (plugin.middleware) { pluginsMiddleware.push(plugin.middleware); } } }; const graphql = resolveLazyObject(graphql$); const config = resolveLazyObject(config$); const plugins = [useSentry(), useViewer(), ...config?.plugins || []]; if (config?.landingPage ?? true) { plugins.push( useUnhandledRoute({ graphqlEndpoint: "/graphql" }) ); } loadPluginsMiddleware(plugins); if (!typeDefs) { const schemaPath = path2.join(process.cwd(), ".pylon", "schema.graphql"); if (schemaPath) { typeDefs = readFileSync(schemaPath, "utf-8"); } } if (!typeDefs) { throw new Error("No schema provided."); } if (!resolvers) { const resolversPath = path2.join(process.cwd(), ".pylon", "resolvers.js"); if (resolversPath) { resolvers = __require(resolversPath).resolvers; } } const graphqlResolvers = resolversToGraphQLResolvers(graphql); const schema = createSchema({ typeDefs, resolvers: { ...graphqlResolvers, ...resolvers, // Transforms a date object to a timestamp Date: DateTimeISOResolver, JSON: JSONResolver, Object: JSONObjectResolver, Void: GraphQLVoid, Number: new GraphQLScalarType({ name: "Number", description: "Custom scalar that handles both integers and floats", // Parsing input from query variables parseValue(value) { if (typeof value !== "number") { throw new TypeError(`Value is not a number: ${value}`); } return value; }, // Validation when sending from client (input literals) parseLiteral(ast) { if (ast.kind === Kind2.INT || ast.kind === Kind2.FLOAT) { return parseFloat(ast.value); } throw new TypeError( `Value is not a valid number or float: ${"value" in ast ? ast.value : ast}` ); }, // Serialize output to be sent to the client serialize(value) { if (typeof value !== "number") { throw new TypeError(`Value is not a number: ${value}`); } return value; } }) } }); const yoga = createYoga({ landingPage: false, graphiql: (req) => { return { shouldPersistHeaders: true, title: "Pylon Playground", defaultQuery: `# Welcome to the Pylon Playground!` }; }, graphqlEndpoint: "/graphql", ...config, plugins, schema }); const handler2 = async (c) => { let executionContext = {}; try { executionContext = c.executionCtx; } catch (e) { } const response = await yoga.fetch(c.req.raw, c.env, executionContext); return c.newResponse(response.body, response); }; return handler2; }; // src/get-env.ts import { sendFunctionEvent as sendFunctionEvent3 } from "@getcronit/pylon-telemetry"; function getEnv() { const start = Date.now(); const skipTracing = arguments[0] === true; try { const context = asyncContext.getStore(); const ctx = context.env || process.env || {}; ctx.NODE_ENV = ctx.NODE_ENV || process.env.NODE_ENV || "development"; return ctx; } catch { return process.env; } finally { if (!skipTracing) { sendFunctionEvent3({ name: "getEnv", duration: Date.now() - start }).then(() => { }); } } } // src/index.ts import { createPubSub } from "graphql-yoga"; // src/plugins/use-pages/setup/index.tsx import fs2 from "fs"; import path3 from "path"; import reactServer from "react-dom/server"; import { Readable } from "stream"; // src/plugins/use-pages/setup/app-loader.tsx import { useMemo } from "react"; import { jsx } from "react/jsx-runtime"; var AppLoader = (props) => { props.client.useHydrateCache({ cacheSnapshot: props.pylonData.cacheSnapshot }); const data = props.client.useQuery(); const page = useMemo(() => { const page2 = /* @__PURE__ */ jsx( props.App, { pageProps: { ...props.pylonData.pageProps, data } } ); return page2; }, [props]); return /* @__PURE__ */ jsx(props.Router, { ...props.routerProps, children: page }); }; // src/plugins/use-pages/setup/index.tsx import { trimTrailingSlash } from "hono/trailing-slash"; import { StaticRouter } from "react-router"; import sharp from "sharp"; import { createHash } from "crypto"; import { tmpdir } from "os"; import { pipeline } from "stream/promises"; import { jsx as jsx2 } from "react/jsx-runtime"; var disableCacheMiddleware = async (c, next) => { const env3 = getEnv(); if (env3.NODE_ENV === "development") { c.header( "Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate" ); c.header("Pragma", "no-cache"); c.header("Expires", "0"); c.header("Surrogate-Control", "no-store"); } return next(); }; var setup = (app2) => { const pagesFilePath = path3.resolve(process.cwd(), ".pylon", "pages.json"); let pageRoutes = []; try { pageRoutes = JSON.parse(fs2.readFileSync(pagesFilePath, "utf-8")); } catch (error) { console.error("Error reading pages.json", error); } app2.use(trimTrailingSlash()); let App = void 0; let client = void 0; app2.on( "GET", pageRoutes.map((pageRoute) => pageRoute.slug), disableCacheMiddleware, async (c) => { if (!App) { const module = await import(`${process.cwd()}/.pylon/__pylon/pages/app.js`); App = module.default; } if (!client) { client = await import(`${process.cwd()}/.pylon/client`); } const pageProps = { params: c.req.param(), searchParams: c.req.query(), path: c.req.path }; let cacheSnapshot = void 0; const prepared = await client.prepareReactRender( /* @__PURE__ */ jsx2( AppLoader, { Router: StaticRouter, routerProps: { location: c.req.path }, App, client, pylonData: { pageProps, cacheSnapshot: void 0 } } ) ); cacheSnapshot = prepared.cacheSnapshot; const stream = await reactServer.renderToReadableStream( /* @__PURE__ */ jsx2( AppLoader, { Router: StaticRouter, routerProps: { location: c.req.path }, App, client, pylonData: { pageProps, cacheSnapshot: prepared.cacheSnapshot } } ), { bootstrapModules: ["/__pylon/static/app.js"], bootstrapScriptContent: `window.__PYLON_DATA__ = ${JSON.stringify({ pageProps, cacheSnapshot })}` } ); return c.body(stream); } ); const publicFilesPath = path3.resolve(process.cwd(), ".pylon", "__pylon", "public"); let publicFiles = []; try { publicFiles = fs2.readdirSync(publicFilesPath); } catch (error) { console.error("Error reading public files", error); } app2.on( "GET", publicFiles.map((file) => `/${file}`), disableCacheMiddleware, async (c) => { const publicFilePath = path3.resolve( process.cwd(), ".pylon", "__pylon", "public", c.req.path.replace("/", "") ); try { await fs2.promises.access(publicFilePath); if (publicFilePath.endsWith(".js")) { c.res.headers.set("Content-Type", "text/javascript"); } else if (publicFilePath.endsWith(".css")) { c.res.headers.set("Content-Type", "text/css"); } else if (publicFilePath.endsWith(".html")) { c.res.headers.set("Content-Type", "text/html"); } else if (publicFilePath.endsWith(".json")) { c.res.headers.set("Content-Type", "application/json"); } else if (publicFilePath.endsWith(".png")) { c.res.headers.set("Content-Type", "image/png"); } else if (publicFilePath.endsWith(".jpg") || publicFilePath.endsWith(".jpeg")) { c.res.headers.set("Content-Type", "image/jpeg"); } else if (publicFilePath.endsWith(".gif")) { c.res.headers.set("Content-Type", "image/gif"); } else if (publicFilePath.endsWith(".svg")) { c.res.headers.set("Content-Type", "image/svg+xml"); } else if (publicFilePath.endsWith(".ico")) { c.res.headers.set("Content-Type", "image/x-icon"); } const stream = fs2.createReadStream(publicFilePath); const a = Readable.toWeb(stream); return c.body(a); } catch { return c.status(404); } } ); app2.get("/__pylon/static/*", disableCacheMiddleware, async (c) => { const filePath = path3.resolve( process.cwd(), ".pylon", "__pylon", "static", c.req.path.replace("/__pylon/static/", "") ); if (!fs2.existsSync(filePath)) { return c.status(404); } if (filePath.endsWith(".js")) { c.res.headers.set("Content-Type", "text/javascript"); } else if (filePath.endsWith(".css")) { c.res.headers.set("Content-Type", "text/css"); } else if (filePath.endsWith(".html")) { c.res.headers.set("Content-Type", "text/html"); } else if (filePath.endsWith(".json")) { c.res.headers.set("Content-Type", "application/json"); } else if (filePath.endsWith(".png")) { c.res.headers.set("Content-Type", "image/png"); } else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) { c.res.headers.set("Content-Type", "image/jpeg"); } else if (filePath.endsWith(".gif")) { c.res.headers.set("Content-Type", "image/gif"); } else if (filePath.endsWith(".svg")) { c.res.headers.set("Content-Type", "image/svg+xml"); } else if (filePath.endsWith(".ico")) { c.res.headers.set("Content-Type", "image/x-icon"); } const stream = fs2.createReadStream(filePath); const a = Readable.toWeb(stream); return c.body(a); }); app2.get("/__pylon/image", async (c) => { try { const { src, w, h, q = "75", format = "webp" } = c.req.query(); const queryStringHash = createHash("sha256").update(JSON.stringify(c.req.query())).digest("hex"); if (!src) { return c.json({ error: "Missing parameters." }, 400); } let imagePath = path3.join(process.cwd(), ".pylon", src); if (src.startsWith("http://") || src.startsWith("https://")) { imagePath = await downloadImage(src); } try { await fs2.promises.access(imagePath); } catch { return c.json({ error: "Image not found" }, 404); } const metadata = await sharp(imagePath).metadata(); if (!metadata.width || !metadata.height) { return c.json( { error: "Invalid image metadata. Width and height are required for resizing." }, 400 ); } const { width: finalWidth, height: finalHeight } = calculateDimensions( metadata.width, metadata.height