@getcronit/pylon
Version:

1,445 lines (1,401 loc) • 65.8 kB
JavaScript
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