@getcronit/pylon 2.10.0-canary-20250206024306.0b965bfde9f8e1db5b2728aeb8259def3c857325 → 3.0.0-canary-20250211153808.b2b63f4e67c55542413f7be6d62ce8139cfcbdbe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context.d.ts +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +938 -241
- package/dist/index.js.map +4 -4
- package/dist/plugins/use-auth/auth-require.d.ts +10 -0
- package/dist/plugins/use-auth/import-private-key.d.ts +2 -0
- package/dist/plugins/use-auth/index.d.ts +3 -0
- package/dist/plugins/use-auth/types.d.ts +7 -0
- package/dist/plugins/use-auth/use-auth.d.ts +6 -0
- package/dist/plugins/use-pages/build/app-utils.d.ts +7 -0
- package/dist/plugins/use-pages/build/index.d.ts +2 -0
- package/dist/plugins/use-pages/build/plugins/image-plugin.d.ts +2 -0
- package/dist/plugins/use-pages/build/plugins/inject-app-hydration.d.ts +2 -0
- package/dist/plugins/use-pages/build/plugins/postcss-plugin.d.ts +2 -0
- package/dist/plugins/use-pages/index.d.ts +4 -0
- package/dist/plugins/use-pages/setup/app-loader.d.ts +14 -0
- package/dist/plugins/use-pages/setup/index.d.ts +10 -0
- package/package.json +32 -4
- package/dist/auth/decorators/requireAuth.d.ts +0 -5
- package/dist/auth/index.d.ts +0 -21
package/dist/index.js
CHANGED
|
@@ -148,13 +148,13 @@ var resolversToGraphQLResolvers = (resolvers, configureContext) => {
|
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
150
|
ctx2?.set("graphqlResolveInfo", info);
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
151
|
+
const auth = ctx2?.get("auth");
|
|
152
|
+
if (auth?.user) {
|
|
153
153
|
scope.setUser({
|
|
154
|
-
id:
|
|
155
|
-
username:
|
|
156
|
-
email:
|
|
157
|
-
details:
|
|
154
|
+
id: auth.user.sub,
|
|
155
|
+
username: auth.user.preferred_username,
|
|
156
|
+
email: auth.user.email,
|
|
157
|
+
details: auth.user
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
let type = null;
|
|
@@ -273,18 +273,246 @@ var ServiceError = class extends GraphQLError {
|
|
|
273
273
|
}
|
|
274
274
|
};
|
|
275
275
|
|
|
276
|
-
// src/auth/
|
|
277
|
-
import
|
|
276
|
+
// src/plugins/use-auth/use-auth.ts
|
|
277
|
+
import { promises as fs } from "fs";
|
|
278
|
+
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
279
|
+
import { HTTPException } from "hono/http-exception";
|
|
280
|
+
import * as openid from "openid-client";
|
|
278
281
|
import path from "path";
|
|
279
|
-
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
280
|
-
import { env as env2 } from "hono/adapter";
|
|
281
|
-
import * as Sentry2 from "@sentry/bun";
|
|
282
|
-
import { existsSync, readFileSync } from "fs";
|
|
283
|
-
import { sendFunctionEvent as sendFunctionEvent4 } from "@getcronit/pylon-telemetry";
|
|
284
282
|
|
|
285
|
-
// src/auth/
|
|
286
|
-
import
|
|
287
|
-
|
|
283
|
+
// src/plugins/use-auth/import-private-key.ts
|
|
284
|
+
import * as crypto2 from "crypto";
|
|
285
|
+
function str2ab(str) {
|
|
286
|
+
const buf = new ArrayBuffer(str.length);
|
|
287
|
+
const bufView = new Uint8Array(buf);
|
|
288
|
+
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
|
289
|
+
bufView[i] = str.charCodeAt(i);
|
|
290
|
+
}
|
|
291
|
+
return buf;
|
|
292
|
+
}
|
|
293
|
+
var convertPKCS1ToPKCS8 = (pkcs1) => {
|
|
294
|
+
const key = crypto2.createPrivateKey(pkcs1);
|
|
295
|
+
return key.export({
|
|
296
|
+
type: "pkcs8",
|
|
297
|
+
format: "pem"
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
function importPKCS8PrivateKey(pem) {
|
|
301
|
+
const pemHeader = "-----BEGIN PRIVATE KEY-----";
|
|
302
|
+
const pemFooter = "-----END PRIVATE KEY-----";
|
|
303
|
+
const pemContents = pem.substring(
|
|
304
|
+
pemHeader.length,
|
|
305
|
+
pem.length - pemFooter.length - 1
|
|
306
|
+
);
|
|
307
|
+
const binaryDerString = atob(pemContents);
|
|
308
|
+
const binaryDer = str2ab(binaryDerString);
|
|
309
|
+
return crypto2.subtle.importKey(
|
|
310
|
+
"pkcs8",
|
|
311
|
+
binaryDer,
|
|
312
|
+
{
|
|
313
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
314
|
+
hash: "SHA-256"
|
|
315
|
+
},
|
|
316
|
+
true,
|
|
317
|
+
["sign"]
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
var importPrivateKey = async (pkcs1Pem) => {
|
|
321
|
+
const pkcs8Pem = convertPKCS1ToPKCS8(pkcs1Pem);
|
|
322
|
+
return await importPKCS8PrivateKey(pkcs8Pem);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/plugins/use-auth/use-auth.ts
|
|
326
|
+
var loadAuthKey = async (keyPath) => {
|
|
327
|
+
const authKeyFilePath = path.join(process.cwd(), keyPath);
|
|
328
|
+
const env3 = getContext().env;
|
|
329
|
+
if (env3.AUTH_KEY) {
|
|
330
|
+
try {
|
|
331
|
+
return JSON.parse(env3.AUTH_KEY);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
"Error while reading AUTH_KEY. Make sure it is valid JSON"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const ketFileContent = await fs.readFile(authKeyFilePath, "utf-8");
|
|
340
|
+
try {
|
|
341
|
+
return JSON.parse(ketFileContent);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"Error while reading key file. Make sure it is valid JSON"
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
throw new Error("Error while reading key file. Make sure it exists");
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var openidConfigCache;
|
|
352
|
+
var bootstrapAuth = async (issuer, keyPath) => {
|
|
353
|
+
if (!openidConfigCache) {
|
|
354
|
+
const authKey = await loadAuthKey(keyPath);
|
|
355
|
+
openidConfigCache = await openid.discovery(
|
|
356
|
+
new URL(issuer),
|
|
357
|
+
authKey.clientId,
|
|
358
|
+
void 0,
|
|
359
|
+
openid.PrivateKeyJwt({
|
|
360
|
+
key: await importPrivateKey(authKey.key),
|
|
361
|
+
kid: authKey.keyId
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return openidConfigCache;
|
|
366
|
+
};
|
|
367
|
+
var PylonAuthException = class extends HTTPException {
|
|
368
|
+
// Same constructor as HTTPException
|
|
369
|
+
constructor(...args) {
|
|
370
|
+
args[1] = {
|
|
371
|
+
...args[1],
|
|
372
|
+
message: `PylonAuthException: ${args[1]?.message}`
|
|
373
|
+
};
|
|
374
|
+
super(...args);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
function useAuth(args) {
|
|
378
|
+
const { issuer, endpoint = "/auth", keyPath = "key.json" } = args;
|
|
379
|
+
const loginPath = `${endpoint}/login`;
|
|
380
|
+
const logoutPath = `${endpoint}/logout`;
|
|
381
|
+
const callbackPath = `${endpoint}/callback`;
|
|
382
|
+
return {
|
|
383
|
+
middleware: async (ctx, next) => {
|
|
384
|
+
const openidConfig = await bootstrapAuth(issuer, keyPath);
|
|
385
|
+
ctx.set("auth", { openidConfig });
|
|
386
|
+
const authCookieToken = getCookie(ctx, "pylon-auth");
|
|
387
|
+
const authHeader = ctx.req.header("Authorization");
|
|
388
|
+
const authQueryToken = ctx.req.query("token");
|
|
389
|
+
if (authCookieToken || authHeader || authQueryToken) {
|
|
390
|
+
let token;
|
|
391
|
+
if (authHeader) {
|
|
392
|
+
const [type, value] = authHeader.split(" ");
|
|
393
|
+
if (type === "Bearer") {
|
|
394
|
+
token = value;
|
|
395
|
+
}
|
|
396
|
+
} else if (authQueryToken) {
|
|
397
|
+
token = authQueryToken;
|
|
398
|
+
} else if (authCookieToken) {
|
|
399
|
+
token = authCookieToken;
|
|
400
|
+
}
|
|
401
|
+
if (!token) {
|
|
402
|
+
throw new PylonAuthException(401, {
|
|
403
|
+
message: "Invalid token"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const introspection = await openid.tokenIntrospection(
|
|
407
|
+
openidConfig,
|
|
408
|
+
token,
|
|
409
|
+
{
|
|
410
|
+
scope: "openid email profile"
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
if (!introspection.active) {
|
|
414
|
+
throw new PylonAuthException(401, {
|
|
415
|
+
message: "Token is not active"
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (!introspection.sub) {
|
|
419
|
+
throw new PylonAuthException(401, {
|
|
420
|
+
message: "Token is missing subject"
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const userInfo = await openid.fetchUserInfo(
|
|
424
|
+
openidConfig,
|
|
425
|
+
token,
|
|
426
|
+
introspection.sub
|
|
427
|
+
);
|
|
428
|
+
const roles = Object.keys(
|
|
429
|
+
introspection["urn:zitadel:iam:org:projects:roles"]?.valueOf() || {}
|
|
430
|
+
);
|
|
431
|
+
ctx.set("auth", {
|
|
432
|
+
user: {
|
|
433
|
+
...userInfo,
|
|
434
|
+
roles
|
|
435
|
+
},
|
|
436
|
+
openidConfig
|
|
437
|
+
});
|
|
438
|
+
return next();
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
setup(app2) {
|
|
442
|
+
app2.get(loginPath, async (ctx) => {
|
|
443
|
+
const openidConfig = ctx.get("auth").openidConfig;
|
|
444
|
+
const codeVerifier = openid.randomPKCECodeVerifier();
|
|
445
|
+
const codeChallenge = await openid.calculatePKCECodeChallenge(
|
|
446
|
+
codeVerifier
|
|
447
|
+
);
|
|
448
|
+
setCookie(ctx, "pylon_code_verifier", codeVerifier, {
|
|
449
|
+
httpOnly: true,
|
|
450
|
+
maxAge: 300
|
|
451
|
+
// 5 minutes
|
|
452
|
+
});
|
|
453
|
+
let scope = "openid profile email urn:zitadel:iam:user:resourceowner urn:zitadel:iam:org:projects:roles";
|
|
454
|
+
const parameters = {
|
|
455
|
+
scope,
|
|
456
|
+
code_challenge: codeChallenge,
|
|
457
|
+
code_challenge_method: "S256",
|
|
458
|
+
redirect_uri: new URL(ctx.req.url).origin + "/auth/callback",
|
|
459
|
+
state: openid.randomState()
|
|
460
|
+
};
|
|
461
|
+
const authorizationUrl = openid.buildAuthorizationUrl(
|
|
462
|
+
openidConfig,
|
|
463
|
+
parameters
|
|
464
|
+
);
|
|
465
|
+
return ctx.redirect(authorizationUrl);
|
|
466
|
+
});
|
|
467
|
+
app2.get(logoutPath, async (ctx) => {
|
|
468
|
+
deleteCookie(ctx, "pylon-auth");
|
|
469
|
+
return ctx.redirect("/");
|
|
470
|
+
});
|
|
471
|
+
app2.get(callbackPath, async (ctx) => {
|
|
472
|
+
const openidConfig = ctx.get("auth").openidConfig;
|
|
473
|
+
const params = ctx.req.query();
|
|
474
|
+
const code = params.code;
|
|
475
|
+
const state = params.state;
|
|
476
|
+
if (!code || !state) {
|
|
477
|
+
throw new PylonAuthException(400, {
|
|
478
|
+
message: "Missing authorization code or state"
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
const codeVerifier = getCookie(ctx, "pylon_code_verifier");
|
|
482
|
+
if (!codeVerifier) {
|
|
483
|
+
throw new PylonAuthException(400, {
|
|
484
|
+
message: "Missing code verifier"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const cbUrl = new URL(ctx.req.url);
|
|
489
|
+
let tokenSet = await openid.authorizationCodeGrant(
|
|
490
|
+
openidConfig,
|
|
491
|
+
cbUrl,
|
|
492
|
+
{
|
|
493
|
+
pkceCodeVerifier: codeVerifier,
|
|
494
|
+
expectedState: state
|
|
495
|
+
},
|
|
496
|
+
cbUrl.searchParams
|
|
497
|
+
);
|
|
498
|
+
setCookie(ctx, `pylon-auth`, tokenSet.access_token, {
|
|
499
|
+
httpOnly: true,
|
|
500
|
+
maxAge: tokenSet.expires_in || 3600
|
|
501
|
+
// Default to 1 hour if not specified
|
|
502
|
+
});
|
|
503
|
+
return ctx.redirect("/");
|
|
504
|
+
} catch (error) {
|
|
505
|
+
console.error("Error during token exchange:", error);
|
|
506
|
+
return ctx.text("Authentication failed!", 500);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/plugins/use-auth/auth-require.ts
|
|
514
|
+
import { env as env2 } from "hono/adapter";
|
|
515
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
288
516
|
|
|
289
517
|
// src/create-decorator.ts
|
|
290
518
|
import { sendFunctionEvent as sendFunctionEvent2 } from "@getcronit/pylon-telemetry";
|
|
@@ -335,20 +563,47 @@ function createDecorator(callback) {
|
|
|
335
563
|
return MyDecorator;
|
|
336
564
|
}
|
|
337
565
|
|
|
338
|
-
// src/auth/
|
|
566
|
+
// src/plugins/use-auth/auth-require.ts
|
|
567
|
+
var authMiddleware = (checks = {}) => {
|
|
568
|
+
const middleware = async (ctx, next) => {
|
|
569
|
+
const AUTH_PROJECT_ID = env2(ctx).AUTH_PROJECT_ID;
|
|
570
|
+
const auth = ctx.get("auth");
|
|
571
|
+
if (!auth) {
|
|
572
|
+
throw new HTTPException2(401, {
|
|
573
|
+
message: "Authentication required"
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (checks.roles && auth.user) {
|
|
577
|
+
const roles = auth.user.roles;
|
|
578
|
+
const hasRole = checks.roles.some((role) => {
|
|
579
|
+
return roles.includes(role) || roles.includes(`${AUTH_PROJECT_ID}:${role}`);
|
|
580
|
+
});
|
|
581
|
+
if (!hasRole) {
|
|
582
|
+
const resError = new Response("Forbidden", {
|
|
583
|
+
status: 403,
|
|
584
|
+
statusText: "Forbidden",
|
|
585
|
+
headers: {
|
|
586
|
+
"Missing-Roles": checks.roles.join(","),
|
|
587
|
+
"Obtained-Roles": roles.join(",")
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
throw new HTTPException2(resError.status, {
|
|
591
|
+
res: resError
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return next();
|
|
596
|
+
};
|
|
597
|
+
return middleware;
|
|
598
|
+
};
|
|
339
599
|
function requireAuth(checks) {
|
|
340
|
-
sendFunctionEvent3({
|
|
341
|
-
name: "requireAuth",
|
|
342
|
-
duration: 0
|
|
343
|
-
}).then(() => {
|
|
344
|
-
});
|
|
345
600
|
const checkAuth = async (c) => {
|
|
346
601
|
const ctx = await c;
|
|
347
602
|
try {
|
|
348
|
-
await
|
|
603
|
+
await authMiddleware(checks)(ctx, async () => {
|
|
349
604
|
});
|
|
350
605
|
} catch (e) {
|
|
351
|
-
if (e instanceof
|
|
606
|
+
if (e instanceof HTTPException2) {
|
|
352
607
|
if (e.status === 401) {
|
|
353
608
|
throw new ServiceError(e.message, {
|
|
354
609
|
statusCode: 401,
|
|
@@ -377,214 +632,11 @@ function requireAuth(checks) {
|
|
|
377
632
|
});
|
|
378
633
|
}
|
|
379
634
|
|
|
380
|
-
// src/auth/index.ts
|
|
381
|
-
var authInitialize = () => {
|
|
382
|
-
const authKeyFilePath = path.join(process.cwd(), "key.json");
|
|
383
|
-
let API_PRIVATE_KEY_FILE = void 0;
|
|
384
|
-
if (existsSync(authKeyFilePath)) {
|
|
385
|
-
try {
|
|
386
|
-
API_PRIVATE_KEY_FILE = JSON.parse(readFileSync(authKeyFilePath, "utf-8"));
|
|
387
|
-
} catch (error) {
|
|
388
|
-
throw new Error(
|
|
389
|
-
"Error while reading key file. Make sure it is valid JSON"
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const middleware = Sentry2.startSpan(
|
|
394
|
-
{
|
|
395
|
-
name: "AuthMiddleware",
|
|
396
|
-
op: "auth"
|
|
397
|
-
},
|
|
398
|
-
() => async function(ctx, next) {
|
|
399
|
-
const AUTH_ISSUER = env2(ctx).AUTH_ISSUER;
|
|
400
|
-
if (!AUTH_ISSUER) {
|
|
401
|
-
throw new Error("AUTH_ISSUER is not set");
|
|
402
|
-
}
|
|
403
|
-
if (!API_PRIVATE_KEY_FILE) {
|
|
404
|
-
const AUTH_KEY = env2(ctx).AUTH_KEY;
|
|
405
|
-
API_PRIVATE_KEY_FILE = AUTH_KEY ? JSON.parse(AUTH_KEY) : void 0;
|
|
406
|
-
}
|
|
407
|
-
if (!API_PRIVATE_KEY_FILE) {
|
|
408
|
-
throw new Error(
|
|
409
|
-
"You have initialized the auth middleware without a private key file"
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
const AUTH_PROJECT_ID = env2(ctx).AUTH_PROJECT_ID;
|
|
413
|
-
const ZITADEL_INTROSPECTION_URL = `${AUTH_ISSUER}/oauth/v2/introspect`;
|
|
414
|
-
async function getRolesFromToken(tokenString) {
|
|
415
|
-
const response = await fetch(
|
|
416
|
-
`${AUTH_ISSUER}/auth/v1/usergrants/me/_search`,
|
|
417
|
-
{
|
|
418
|
-
method: "POST",
|
|
419
|
-
headers: {
|
|
420
|
-
"Content-Type": "application/json",
|
|
421
|
-
Authorization: `Bearer ${tokenString}`
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
);
|
|
425
|
-
const data = await response.json();
|
|
426
|
-
const userRoles = data.result?.map((grant) => {
|
|
427
|
-
return (grant.roles || []).map((role) => {
|
|
428
|
-
return `${grant.projectId}:${role}`;
|
|
429
|
-
});
|
|
430
|
-
}) || [];
|
|
431
|
-
const projectScopedRoles = userRoles.flat();
|
|
432
|
-
const rolesSet = new Set(projectScopedRoles);
|
|
433
|
-
if (AUTH_PROJECT_ID) {
|
|
434
|
-
for (const role of projectScopedRoles) {
|
|
435
|
-
const [projectId, ...roleNameParts] = role.split(":");
|
|
436
|
-
const roleName = roleNameParts.join(":");
|
|
437
|
-
if (projectId === AUTH_PROJECT_ID) {
|
|
438
|
-
rolesSet.add(roleName);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return Array.from(rolesSet);
|
|
443
|
-
}
|
|
444
|
-
async function introspectToken(tokenString) {
|
|
445
|
-
if (!API_PRIVATE_KEY_FILE) {
|
|
446
|
-
throw new Error("Internal error: API_PRIVATE_KEY_FILE is not set");
|
|
447
|
-
}
|
|
448
|
-
const payload = {
|
|
449
|
-
iss: API_PRIVATE_KEY_FILE.clientId,
|
|
450
|
-
sub: API_PRIVATE_KEY_FILE.clientId,
|
|
451
|
-
aud: AUTH_ISSUER,
|
|
452
|
-
exp: Math.floor(Date.now() / 1e3) + 60 * 60,
|
|
453
|
-
// Expires in 1 hour
|
|
454
|
-
iat: Math.floor(Date.now() / 1e3)
|
|
455
|
-
};
|
|
456
|
-
const headers = {
|
|
457
|
-
alg: "RS256",
|
|
458
|
-
kid: API_PRIVATE_KEY_FILE.keyId
|
|
459
|
-
};
|
|
460
|
-
const jwtToken = jwt.sign(payload, API_PRIVATE_KEY_FILE.key, {
|
|
461
|
-
algorithm: "RS256",
|
|
462
|
-
header: headers
|
|
463
|
-
});
|
|
464
|
-
const scopeSet = /* @__PURE__ */ new Set();
|
|
465
|
-
scopeSet.add("openid");
|
|
466
|
-
scopeSet.add("profile");
|
|
467
|
-
scopeSet.add("email");
|
|
468
|
-
if (AUTH_PROJECT_ID) {
|
|
469
|
-
scopeSet.add(
|
|
470
|
-
`urn:zitadel:iam:org:project:id:${AUTH_PROJECT_ID}:aud`
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
const scope = Array.from(scopeSet).join(" ");
|
|
474
|
-
const body = new URLSearchParams({
|
|
475
|
-
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
476
|
-
client_assertion: jwtToken,
|
|
477
|
-
token: tokenString,
|
|
478
|
-
scope
|
|
479
|
-
}).toString();
|
|
480
|
-
try {
|
|
481
|
-
const response = await fetch(ZITADEL_INTROSPECTION_URL, {
|
|
482
|
-
method: "POST",
|
|
483
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
484
|
-
body
|
|
485
|
-
});
|
|
486
|
-
if (!response.ok) {
|
|
487
|
-
throw new Error("Network response was not ok");
|
|
488
|
-
}
|
|
489
|
-
const tokenData = await response.json();
|
|
490
|
-
const roles = await getRolesFromToken(tokenString);
|
|
491
|
-
const state = {
|
|
492
|
-
...tokenData,
|
|
493
|
-
roles
|
|
494
|
-
};
|
|
495
|
-
return state;
|
|
496
|
-
} catch (error) {
|
|
497
|
-
console.error("Error while introspecting token", error);
|
|
498
|
-
throw new Error("Token introspection failed");
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
let token = void 0;
|
|
502
|
-
if (ctx.req.header("Authorization")) {
|
|
503
|
-
const authHeader = ctx.req.header("Authorization");
|
|
504
|
-
if (authHeader) {
|
|
505
|
-
const parts = authHeader.split(" ");
|
|
506
|
-
if (parts.length === 2 && parts[0] === "Bearer") {
|
|
507
|
-
token = parts[1];
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
if (!token) {
|
|
512
|
-
const queryToken = ctx.req.query("token");
|
|
513
|
-
if (queryToken) {
|
|
514
|
-
token = queryToken;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (token) {
|
|
518
|
-
const auth2 = await introspectToken(token);
|
|
519
|
-
if (auth2.active) {
|
|
520
|
-
ctx.set("auth", auth2);
|
|
521
|
-
Sentry2.setUser({
|
|
522
|
-
id: auth2.sub,
|
|
523
|
-
username: auth2.preferred_username,
|
|
524
|
-
email: auth2.email,
|
|
525
|
-
details: auth2
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return next();
|
|
530
|
-
}
|
|
531
|
-
);
|
|
532
|
-
sendFunctionEvent4({
|
|
533
|
-
name: "authInitialize",
|
|
534
|
-
duration: 0
|
|
535
|
-
}).then(() => {
|
|
536
|
-
});
|
|
537
|
-
return middleware;
|
|
538
|
-
};
|
|
539
|
-
var authRequire = (checks = {}) => {
|
|
540
|
-
sendFunctionEvent4({
|
|
541
|
-
name: "authRequire",
|
|
542
|
-
duration: 0
|
|
543
|
-
}).then(() => {
|
|
544
|
-
});
|
|
545
|
-
const middleware = async (ctx, next) => {
|
|
546
|
-
const AUTH_PROJECT_ID = env2(ctx).AUTH_PROJECT_ID;
|
|
547
|
-
const auth2 = ctx.get("auth");
|
|
548
|
-
if (!auth2) {
|
|
549
|
-
throw new HTTPException2(401, {
|
|
550
|
-
message: "Authentication required"
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
if (checks.roles) {
|
|
554
|
-
const roles = auth2.roles;
|
|
555
|
-
const hasRole = checks.roles.some((role) => {
|
|
556
|
-
return roles.includes(role) || roles.includes(`${AUTH_PROJECT_ID}:${role}`);
|
|
557
|
-
});
|
|
558
|
-
if (!hasRole) {
|
|
559
|
-
const resError = new Response("Forbidden", {
|
|
560
|
-
status: 403,
|
|
561
|
-
statusText: "Forbidden",
|
|
562
|
-
headers: {
|
|
563
|
-
"Missing-Roles": checks.roles.join(","),
|
|
564
|
-
"Obtained-Roles": roles.join(",")
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
throw new HTTPException2(resError.status, { res: resError });
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
return next();
|
|
571
|
-
};
|
|
572
|
-
sendFunctionEvent4({
|
|
573
|
-
name: "authRequire",
|
|
574
|
-
duration: 0
|
|
575
|
-
}).then(() => {
|
|
576
|
-
});
|
|
577
|
-
return middleware;
|
|
578
|
-
};
|
|
579
|
-
var auth = {
|
|
580
|
-
initialize: authInitialize,
|
|
581
|
-
require: authRequire
|
|
582
|
-
};
|
|
583
|
-
|
|
584
635
|
// src/app/index.ts
|
|
585
636
|
import { Hono } from "hono";
|
|
586
637
|
import { logger } from "hono/logger";
|
|
587
638
|
import { sentry } from "@hono/sentry";
|
|
639
|
+
import { except } from "hono/combine";
|
|
588
640
|
var app = new Hono();
|
|
589
641
|
app.use("*", sentry());
|
|
590
642
|
app.use("*", async (c, next) => {
|
|
@@ -598,7 +650,7 @@ app.use("*", async (c, next) => {
|
|
|
598
650
|
});
|
|
599
651
|
});
|
|
600
652
|
});
|
|
601
|
-
app.use("*", logger());
|
|
653
|
+
app.use("*", except(["/__pylon/static/*"], logger()));
|
|
602
654
|
app.use((c, next) => {
|
|
603
655
|
c.req.id = crypto.randomUUID();
|
|
604
656
|
return next();
|
|
@@ -633,7 +685,7 @@ import {
|
|
|
633
685
|
handleStreamOrSingleExecutionResult,
|
|
634
686
|
isOriginalGraphQLError
|
|
635
687
|
} from "@envelop/core";
|
|
636
|
-
import * as
|
|
688
|
+
import * as Sentry2 from "@sentry/node";
|
|
637
689
|
var defaultSkipError = isOriginalGraphQLError;
|
|
638
690
|
var useSentry = (options = {}) => {
|
|
639
691
|
function pick(key, defaultValue) {
|
|
@@ -673,7 +725,7 @@ var useSentry = (options = {}) => {
|
|
|
673
725
|
...addedTags
|
|
674
726
|
};
|
|
675
727
|
if (options.configureScope) {
|
|
676
|
-
options.configureScope(args,
|
|
728
|
+
options.configureScope(args, Sentry2.getCurrentScope());
|
|
677
729
|
}
|
|
678
730
|
return {
|
|
679
731
|
onExecuteDone(payload) {
|
|
@@ -681,7 +733,7 @@ var useSentry = (options = {}) => {
|
|
|
681
733
|
result,
|
|
682
734
|
setResult
|
|
683
735
|
}) => {
|
|
684
|
-
|
|
736
|
+
Sentry2.startSpanManual(
|
|
685
737
|
{
|
|
686
738
|
op,
|
|
687
739
|
name: opName,
|
|
@@ -696,7 +748,7 @@ var useSentry = (options = {}) => {
|
|
|
696
748
|
span.setAttribute("result", JSON.stringify(result));
|
|
697
749
|
}
|
|
698
750
|
if (result.errors && result.errors.length > 0) {
|
|
699
|
-
|
|
751
|
+
Sentry2.withScope((scope) => {
|
|
700
752
|
scope.setTransactionName(opName);
|
|
701
753
|
scope.setTag("operation", operationType);
|
|
702
754
|
scope.setTag("operationName", opName);
|
|
@@ -722,7 +774,7 @@ var useSentry = (options = {}) => {
|
|
|
722
774
|
level: "debug"
|
|
723
775
|
});
|
|
724
776
|
}
|
|
725
|
-
const eventId =
|
|
777
|
+
const eventId = Sentry2.captureException(
|
|
726
778
|
err.originalError,
|
|
727
779
|
{
|
|
728
780
|
fingerprint: [
|
|
@@ -760,7 +812,7 @@ var useSentry = (options = {}) => {
|
|
|
760
812
|
};
|
|
761
813
|
|
|
762
814
|
// src/app/pylon-handler.ts
|
|
763
|
-
import { readFileSync
|
|
815
|
+
import { readFileSync } from "fs";
|
|
764
816
|
import path2 from "path";
|
|
765
817
|
|
|
766
818
|
// src/plugins/use-viewer.ts
|
|
@@ -1055,7 +1107,7 @@ var handler = (options) => {
|
|
|
1055
1107
|
if (!typeDefs) {
|
|
1056
1108
|
const schemaPath = path2.join(process.cwd(), ".pylon", "schema.graphql");
|
|
1057
1109
|
if (schemaPath) {
|
|
1058
|
-
typeDefs =
|
|
1110
|
+
typeDefs = readFileSync(schemaPath, "utf-8");
|
|
1059
1111
|
}
|
|
1060
1112
|
}
|
|
1061
1113
|
if (!typeDefs) {
|
|
@@ -1134,18 +1186,20 @@ var handler = (options) => {
|
|
|
1134
1186
|
};
|
|
1135
1187
|
|
|
1136
1188
|
// src/get-env.ts
|
|
1137
|
-
import { sendFunctionEvent as
|
|
1189
|
+
import { sendFunctionEvent as sendFunctionEvent3 } from "@getcronit/pylon-telemetry";
|
|
1138
1190
|
function getEnv() {
|
|
1139
1191
|
const start = Date.now();
|
|
1140
1192
|
const skipTracing = arguments[0] === true;
|
|
1141
1193
|
try {
|
|
1142
1194
|
const context = asyncContext.getStore();
|
|
1143
|
-
|
|
1195
|
+
const ctx = context.env || process.env || {};
|
|
1196
|
+
ctx.NODE_ENV = ctx.NODE_ENV || process.env.NODE_ENV || "development";
|
|
1197
|
+
return ctx;
|
|
1144
1198
|
} catch {
|
|
1145
1199
|
return process.env;
|
|
1146
1200
|
} finally {
|
|
1147
1201
|
if (!skipTracing) {
|
|
1148
|
-
|
|
1202
|
+
sendFunctionEvent3({
|
|
1149
1203
|
name: "getEnv",
|
|
1150
1204
|
duration: Date.now() - start
|
|
1151
1205
|
}).then(() => {
|
|
@@ -1156,17 +1210,660 @@ function getEnv() {
|
|
|
1156
1210
|
|
|
1157
1211
|
// src/index.ts
|
|
1158
1212
|
import { createPubSub } from "graphql-yoga";
|
|
1213
|
+
|
|
1214
|
+
// src/plugins/use-pages/setup/index.tsx
|
|
1215
|
+
import fs2 from "fs";
|
|
1216
|
+
import path3 from "path";
|
|
1217
|
+
import reactServer from "react-dom/server";
|
|
1218
|
+
import { Readable } from "stream";
|
|
1219
|
+
|
|
1220
|
+
// src/plugins/use-pages/setup/app-loader.tsx
|
|
1221
|
+
import { useMemo } from "react";
|
|
1222
|
+
import { jsx } from "react/jsx-runtime";
|
|
1223
|
+
var AppLoader = (props) => {
|
|
1224
|
+
props.client.useHydrateCache({ cacheSnapshot: props.pylonData.cacheSnapshot });
|
|
1225
|
+
const data = props.client.useQuery();
|
|
1226
|
+
const page = useMemo(() => {
|
|
1227
|
+
const page2 = /* @__PURE__ */ jsx(
|
|
1228
|
+
props.App,
|
|
1229
|
+
{
|
|
1230
|
+
pageProps: {
|
|
1231
|
+
...props.pylonData.pageProps,
|
|
1232
|
+
data
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
);
|
|
1236
|
+
return page2;
|
|
1237
|
+
}, [props]);
|
|
1238
|
+
return /* @__PURE__ */ jsx(props.Router, { ...props.routerProps, children: page });
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
// src/plugins/use-pages/setup/index.tsx
|
|
1242
|
+
import { trimTrailingSlash } from "hono/trailing-slash";
|
|
1243
|
+
import { StaticRouter } from "react-router";
|
|
1244
|
+
import sharp from "sharp";
|
|
1245
|
+
import { createHash } from "crypto";
|
|
1246
|
+
import { tmpdir } from "os";
|
|
1247
|
+
import { pipeline } from "stream/promises";
|
|
1248
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1249
|
+
var disableCacheMiddleware = async (c, next) => {
|
|
1250
|
+
if (c.env.NODE_ENV === "development") {
|
|
1251
|
+
c.header(
|
|
1252
|
+
"Cache-Control",
|
|
1253
|
+
"no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
1254
|
+
);
|
|
1255
|
+
c.header("Pragma", "no-cache");
|
|
1256
|
+
c.header("Expires", "0");
|
|
1257
|
+
c.header("Surrogate-Control", "no-store");
|
|
1258
|
+
}
|
|
1259
|
+
return next();
|
|
1260
|
+
};
|
|
1261
|
+
var setup = (app2) => {
|
|
1262
|
+
const pagesFilePath = path3.resolve(process.cwd(), ".pylon", "pages.json");
|
|
1263
|
+
let pageRoutes = [];
|
|
1264
|
+
try {
|
|
1265
|
+
pageRoutes = JSON.parse(fs2.readFileSync(pagesFilePath, "utf-8"));
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
console.error("Error reading pages.json", error);
|
|
1268
|
+
}
|
|
1269
|
+
app2.use(trimTrailingSlash());
|
|
1270
|
+
let App = void 0;
|
|
1271
|
+
let client = void 0;
|
|
1272
|
+
app2.on(
|
|
1273
|
+
"GET",
|
|
1274
|
+
pageRoutes.map((pageRoute) => pageRoute.slug),
|
|
1275
|
+
disableCacheMiddleware,
|
|
1276
|
+
async (c) => {
|
|
1277
|
+
if (!App) {
|
|
1278
|
+
const module = await import(`${process.cwd()}/.pylon/__pylon/pages/app.js`);
|
|
1279
|
+
App = module.default;
|
|
1280
|
+
}
|
|
1281
|
+
if (!client) {
|
|
1282
|
+
client = await import(`${process.cwd()}/.pylon/client`);
|
|
1283
|
+
}
|
|
1284
|
+
const pageProps = {
|
|
1285
|
+
params: c.req.param(),
|
|
1286
|
+
searchParams: c.req.query(),
|
|
1287
|
+
path: c.req.path
|
|
1288
|
+
};
|
|
1289
|
+
let cacheSnapshot = void 0;
|
|
1290
|
+
const prepared = await client.prepareReactRender(
|
|
1291
|
+
/* @__PURE__ */ jsx2(
|
|
1292
|
+
AppLoader,
|
|
1293
|
+
{
|
|
1294
|
+
Router: StaticRouter,
|
|
1295
|
+
routerProps: {
|
|
1296
|
+
location: c.req.path
|
|
1297
|
+
},
|
|
1298
|
+
App,
|
|
1299
|
+
client,
|
|
1300
|
+
pylonData: {
|
|
1301
|
+
pageProps,
|
|
1302
|
+
cacheSnapshot: void 0
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
1306
|
+
);
|
|
1307
|
+
cacheSnapshot = prepared.cacheSnapshot;
|
|
1308
|
+
const stream = await reactServer.renderToReadableStream(
|
|
1309
|
+
/* @__PURE__ */ jsx2(
|
|
1310
|
+
AppLoader,
|
|
1311
|
+
{
|
|
1312
|
+
Router: StaticRouter,
|
|
1313
|
+
routerProps: {
|
|
1314
|
+
location: c.req.path
|
|
1315
|
+
},
|
|
1316
|
+
App,
|
|
1317
|
+
client,
|
|
1318
|
+
pylonData: {
|
|
1319
|
+
pageProps,
|
|
1320
|
+
cacheSnapshot: prepared.cacheSnapshot
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
),
|
|
1324
|
+
{
|
|
1325
|
+
bootstrapModules: ["/__pylon/static/app.js"],
|
|
1326
|
+
bootstrapScriptContent: `window.__PYLON_DATA__ = ${JSON.stringify({
|
|
1327
|
+
pageProps,
|
|
1328
|
+
cacheSnapshot
|
|
1329
|
+
})}`
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
return c.body(stream);
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1335
|
+
app2.get("/__pylon/static/*", disableCacheMiddleware, async (c) => {
|
|
1336
|
+
const filePath = path3.resolve(
|
|
1337
|
+
process.cwd(),
|
|
1338
|
+
".pylon",
|
|
1339
|
+
"__pylon",
|
|
1340
|
+
"static",
|
|
1341
|
+
c.req.path.replace("/__pylon/static/", "")
|
|
1342
|
+
);
|
|
1343
|
+
if (!fs2.existsSync(filePath)) {
|
|
1344
|
+
throw new Error("File not found");
|
|
1345
|
+
}
|
|
1346
|
+
if (filePath.endsWith(".js")) {
|
|
1347
|
+
c.res.headers.set("Content-Type", "text/javascript");
|
|
1348
|
+
} else if (filePath.endsWith(".css")) {
|
|
1349
|
+
c.res.headers.set("Content-Type", "text/css");
|
|
1350
|
+
} else if (filePath.endsWith(".html")) {
|
|
1351
|
+
c.res.headers.set("Content-Type", "text/html");
|
|
1352
|
+
} else if (filePath.endsWith(".json")) {
|
|
1353
|
+
c.res.headers.set("Content-Type", "application/json");
|
|
1354
|
+
} else if (filePath.endsWith(".png")) {
|
|
1355
|
+
c.res.headers.set("Content-Type", "image/png");
|
|
1356
|
+
} else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) {
|
|
1357
|
+
c.res.headers.set("Content-Type", "image/jpeg");
|
|
1358
|
+
} else if (filePath.endsWith(".gif")) {
|
|
1359
|
+
c.res.headers.set("Content-Type", "image/gif");
|
|
1360
|
+
} else if (filePath.endsWith(".svg")) {
|
|
1361
|
+
c.res.headers.set("Content-Type", "image/svg+xml");
|
|
1362
|
+
} else if (filePath.endsWith(".ico")) {
|
|
1363
|
+
c.res.headers.set("Content-Type", "image/x-icon");
|
|
1364
|
+
}
|
|
1365
|
+
const stream = fs2.createReadStream(filePath);
|
|
1366
|
+
const a = Readable.toWeb(stream);
|
|
1367
|
+
return c.body(a);
|
|
1368
|
+
});
|
|
1369
|
+
app2.get("/__pylon/image", async (c) => {
|
|
1370
|
+
console.log("image optimization route");
|
|
1371
|
+
try {
|
|
1372
|
+
const { src, w, h, q = "75", format = "webp" } = c.req.query();
|
|
1373
|
+
const queryStringHash = createHash("sha256").update(JSON.stringify(c.req.query())).digest("hex");
|
|
1374
|
+
if (!src) {
|
|
1375
|
+
return c.json({ error: "Missing parameters." }, 400);
|
|
1376
|
+
}
|
|
1377
|
+
let imagePath = path3.join(process.cwd(), ".pylon", src);
|
|
1378
|
+
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
1379
|
+
imagePath = await downloadImage(src);
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
console.log("imagePath", imagePath);
|
|
1383
|
+
await fs2.promises.access(imagePath);
|
|
1384
|
+
} catch {
|
|
1385
|
+
return c.json({ error: "Image not found" }, 404);
|
|
1386
|
+
}
|
|
1387
|
+
const metadata = await sharp(imagePath).metadata();
|
|
1388
|
+
if (!metadata.width || !metadata.height) {
|
|
1389
|
+
return c.json(
|
|
1390
|
+
{
|
|
1391
|
+
error: "Invalid image metadata. Width and height are required for resizing."
|
|
1392
|
+
},
|
|
1393
|
+
400
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
const { width: finalWidth, height: finalHeight } = calculateDimensions(
|
|
1397
|
+
metadata.width,
|
|
1398
|
+
metadata.height,
|
|
1399
|
+
w ? parseInt(w) : void 0,
|
|
1400
|
+
h ? parseInt(h) : void 0
|
|
1401
|
+
);
|
|
1402
|
+
const cachePath = path3.join(IMAGE_CACHE_DIR, queryStringHash);
|
|
1403
|
+
let imageFormat = format.toLowerCase();
|
|
1404
|
+
if (!isSupportedFormat(imageFormat)) {
|
|
1405
|
+
throw new Error("Unsupported image format");
|
|
1406
|
+
}
|
|
1407
|
+
const quality = parseInt(q);
|
|
1408
|
+
console.log("quality", quality);
|
|
1409
|
+
const image = await sharp(imagePath).resize(finalWidth, finalHeight).toFormat(imageFormat, {
|
|
1410
|
+
quality
|
|
1411
|
+
}).toFile(cachePath);
|
|
1412
|
+
console.log("image", image);
|
|
1413
|
+
c.res.headers.set("Content-Type", getContentType(image.format));
|
|
1414
|
+
return c.body(
|
|
1415
|
+
Readable.toWeb(fs2.createReadStream(cachePath))
|
|
1416
|
+
);
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
console.error("Error processing the image:", error);
|
|
1419
|
+
return c.json({ error: "Error processing the image" }, 500);
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
};
|
|
1423
|
+
var IMAGE_CACHE_DIR = path3.join(process.cwd(), ".cache/__pylon/images");
|
|
1424
|
+
fs2.promises.mkdir(IMAGE_CACHE_DIR, { recursive: true });
|
|
1425
|
+
var calculateDimensions = (originalWidth, originalHeight, width, height) => {
|
|
1426
|
+
if (!width && !height) {
|
|
1427
|
+
return { width: originalWidth, height: originalHeight };
|
|
1428
|
+
}
|
|
1429
|
+
if (width && !height) {
|
|
1430
|
+
height = Math.round(width * originalHeight / originalWidth);
|
|
1431
|
+
} else if (height && !width) {
|
|
1432
|
+
width = Math.round(height * originalWidth / originalHeight);
|
|
1433
|
+
}
|
|
1434
|
+
return { width, height };
|
|
1435
|
+
};
|
|
1436
|
+
function isSupportedFormat(format) {
|
|
1437
|
+
const supportedFormats = sharp.format;
|
|
1438
|
+
return Object.keys(supportedFormats).includes(format);
|
|
1439
|
+
}
|
|
1440
|
+
var getContentType = (format) => {
|
|
1441
|
+
switch (format.toLowerCase()) {
|
|
1442
|
+
case "webp":
|
|
1443
|
+
return "image/webp";
|
|
1444
|
+
case "jpeg":
|
|
1445
|
+
case "jpg":
|
|
1446
|
+
return "image/jpeg";
|
|
1447
|
+
case "png":
|
|
1448
|
+
return "image/png";
|
|
1449
|
+
case "gif":
|
|
1450
|
+
return "image/gif";
|
|
1451
|
+
case "svg":
|
|
1452
|
+
return "image/svg+xml";
|
|
1453
|
+
default:
|
|
1454
|
+
return "application/octet-stream";
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
var downloadImage = async (url) => {
|
|
1458
|
+
const response = await fetch(url);
|
|
1459
|
+
if (!response.ok)
|
|
1460
|
+
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
1461
|
+
const ext = path3.extname(new URL(url).pathname) || ".jpg";
|
|
1462
|
+
const tempFilePath = path3.join(tmpdir(), `image-${Date.now()}${ext}`);
|
|
1463
|
+
const fileStream = fs2.createWriteStream(tempFilePath);
|
|
1464
|
+
await pipeline(response.body, fileStream);
|
|
1465
|
+
return tempFilePath;
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
// src/plugins/use-pages/build/index.ts
|
|
1469
|
+
import path7 from "path";
|
|
1470
|
+
|
|
1471
|
+
// src/plugins/use-pages/build/app-utils.ts
|
|
1472
|
+
import path4 from "path";
|
|
1473
|
+
import glob from "tiny-glob";
|
|
1474
|
+
function fnv1aHash(str) {
|
|
1475
|
+
let hash = 2166136261;
|
|
1476
|
+
for (let i = 0; i < str.length; i++) {
|
|
1477
|
+
hash ^= str.charCodeAt(i);
|
|
1478
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
1479
|
+
}
|
|
1480
|
+
return (hash >>> 0).toString(16);
|
|
1481
|
+
}
|
|
1482
|
+
var APP_DIR = path4.join(process.cwd(), "pages");
|
|
1483
|
+
async function getPageRoutes(dir = APP_DIR) {
|
|
1484
|
+
const routes = [];
|
|
1485
|
+
const pagePattern = path4.join(dir, "**/page.{ts,tsx,js}");
|
|
1486
|
+
const layoutPattern = path4.join(dir, "**/layout.tsx");
|
|
1487
|
+
const pageFiles = await glob(pagePattern);
|
|
1488
|
+
const layoutFiles = await glob(layoutPattern);
|
|
1489
|
+
for (const pagePath of pageFiles) {
|
|
1490
|
+
const relativePagePath = path4.relative(APP_DIR, pagePath);
|
|
1491
|
+
let slug = "/" + relativePagePath.replace(/page\.(ts|tsx|js)$/, "").replace(/\[([\w-]+)\]/g, ":$1");
|
|
1492
|
+
slug = slug.replace(/\/$/, "");
|
|
1493
|
+
const layouts = layoutFiles.filter((layout) => {
|
|
1494
|
+
return pagePath.startsWith(layout.replace("layout.tsx", ""));
|
|
1495
|
+
});
|
|
1496
|
+
const layoutsWithoutRootLayout = layouts.slice(1);
|
|
1497
|
+
routes.push({
|
|
1498
|
+
pagePath,
|
|
1499
|
+
slug: slug || "/",
|
|
1500
|
+
layouts: layoutsWithoutRootLayout
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
return routes;
|
|
1504
|
+
}
|
|
1505
|
+
var generateAppFile = (pageRoutes) => {
|
|
1506
|
+
const makePageMap = (routes) => {
|
|
1507
|
+
const pageMap2 = {};
|
|
1508
|
+
for (const route of routes) {
|
|
1509
|
+
pageMap2[route.pagePath] = `Page${fnv1aHash(route.pagePath)}`;
|
|
1510
|
+
}
|
|
1511
|
+
return pageMap2;
|
|
1512
|
+
};
|
|
1513
|
+
const makeLayoutMap = (routes) => {
|
|
1514
|
+
const layoutMap2 = {};
|
|
1515
|
+
for (const route of routes) {
|
|
1516
|
+
for (const layout of route.layouts) {
|
|
1517
|
+
layoutMap2[layout] = `Layout${fnv1aHash(layout)}`;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return layoutMap2;
|
|
1521
|
+
};
|
|
1522
|
+
const pageMap = makePageMap(pageRoutes);
|
|
1523
|
+
const layoutMap = makeLayoutMap(pageRoutes);
|
|
1524
|
+
const importPages = Object.keys(pageMap).map((pagePath, index) => {
|
|
1525
|
+
const importLocation = `../${pagePath}`.replace(".tsx", ".js");
|
|
1526
|
+
const componentName = pageMap[pagePath];
|
|
1527
|
+
return `const ${componentName} = lazy(() => import('${importLocation}'))
|
|
1528
|
+
`;
|
|
1529
|
+
}).join("\n");
|
|
1530
|
+
const importLayouts = Object.keys(layoutMap).map((layoutPath, index) => {
|
|
1531
|
+
const importLocation = `../${layoutPath}`.replace(".tsx", ".js");
|
|
1532
|
+
const componentName = layoutMap[layoutPath];
|
|
1533
|
+
return `const ${componentName} = lazy(() => import('${importLocation}'))
|
|
1534
|
+
`;
|
|
1535
|
+
}).join("\n");
|
|
1536
|
+
const appComponent = `"use client";
|
|
1537
|
+
import {lazy, Suspense} from 'react'
|
|
1538
|
+
import { Routes, Route } from 'react-router';
|
|
1539
|
+
${importPages}
|
|
1540
|
+
const RootLayout = lazy(() => import('../pages/layout.js'))
|
|
1541
|
+
${importLayouts}
|
|
1542
|
+
|
|
1543
|
+
const App: React.FC<{pageProps: any}> = ({pageProps}) => (
|
|
1544
|
+
<RootLayout>
|
|
1545
|
+
<meta charSet="utf-8" />
|
|
1546
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1547
|
+
<link rel="stylesheet" href="/__pylon/static/app.css" />
|
|
1548
|
+
<Routes>
|
|
1549
|
+
${pageRoutes.map((route, index) => {
|
|
1550
|
+
return `<Route key={${index}} index={${index === 0 ? "true" : "false"}} path="${route.slug}" element={
|
|
1551
|
+
<Suspense fallback={<div>...</div>}>
|
|
1552
|
+
${route.layouts.reduceRight((child, layoutPath, layoutIndex) => {
|
|
1553
|
+
const layoutName = layoutMap[layoutPath];
|
|
1554
|
+
return `<${layoutName}>${child}</${layoutName}>`;
|
|
1555
|
+
}, `<${pageMap[route.pagePath]} {...pageProps} />`)}
|
|
1556
|
+
|
|
1557
|
+
</Suspense>} />`;
|
|
1558
|
+
}).join("\n")}
|
|
1559
|
+
</Routes>
|
|
1560
|
+
</RootLayout>
|
|
1561
|
+
);
|
|
1562
|
+
|
|
1563
|
+
export default App;
|
|
1564
|
+
`;
|
|
1565
|
+
return appComponent;
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// src/plugins/use-pages/build/index.ts
|
|
1569
|
+
import chokidar from "chokidar";
|
|
1570
|
+
import fs6 from "fs/promises";
|
|
1571
|
+
import esbuild from "esbuild";
|
|
1572
|
+
|
|
1573
|
+
// src/plugins/use-pages/build/plugins/inject-app-hydration.ts
|
|
1574
|
+
import path5 from "path";
|
|
1575
|
+
import fs3 from "fs/promises";
|
|
1576
|
+
var injectAppHydrationPlugin = {
|
|
1577
|
+
name: "inject-hydration",
|
|
1578
|
+
setup(build2) {
|
|
1579
|
+
build2.onLoad({ filter: /.*/, namespace: "file" }, async (args) => {
|
|
1580
|
+
if (args.path === path5.resolve(process.cwd(), ".pylon", "app.tsx")) {
|
|
1581
|
+
let contents = await fs3.readFile(args.path, "utf-8");
|
|
1582
|
+
const clientPath = path5.resolve(process.cwd(), ".pylon/client");
|
|
1583
|
+
const pathToClient = path5.relative(path5.dirname(args.path), clientPath);
|
|
1584
|
+
contents += `
|
|
1585
|
+
import {hydrateRoot} from 'react-dom/client'
|
|
1586
|
+
import * as client from './${pathToClient}'
|
|
1587
|
+
import {BrowserRouter} from 'react-router'
|
|
1588
|
+
import React, {useMemo} from 'react'
|
|
1589
|
+
|
|
1590
|
+
const pylonData = window.__PYLON_DATA__
|
|
1591
|
+
|
|
1592
|
+
console.log('pylonData', pylonData)
|
|
1593
|
+
|
|
1594
|
+
const AppLoader = (props: {
|
|
1595
|
+
client: any
|
|
1596
|
+
pylonData: {
|
|
1597
|
+
pageProps: Omit<PageProps, 'data'>
|
|
1598
|
+
cacheSnapshot?: any
|
|
1599
|
+
}
|
|
1600
|
+
App: React.FC<{
|
|
1601
|
+
pageProps: PageProps
|
|
1602
|
+
}>
|
|
1603
|
+
Router: React.FC<any>
|
|
1604
|
+
routerProps: any
|
|
1605
|
+
}) => {
|
|
1606
|
+
props.client.useHydrateCache({cacheSnapshot: props.pylonData.cacheSnapshot})
|
|
1607
|
+
|
|
1608
|
+
const data = props.client.useQuery()
|
|
1609
|
+
const page = useMemo(() => {
|
|
1610
|
+
const page = (
|
|
1611
|
+
<props.App
|
|
1612
|
+
pageProps={{
|
|
1613
|
+
...props.pylonData.pageProps,
|
|
1614
|
+
data
|
|
1615
|
+
}}
|
|
1616
|
+
/>
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
return page
|
|
1620
|
+
}, [props])
|
|
1621
|
+
|
|
1622
|
+
return <props.Router {...props.routerProps}>{page}</props.Router>
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
hydrateRoot(
|
|
1627
|
+
document,
|
|
1628
|
+
<AppLoader Router={BrowserRouter} client={client} pylonData={pylonData} App={App} />
|
|
1629
|
+
)
|
|
1630
|
+
`;
|
|
1631
|
+
return {
|
|
1632
|
+
loader: "tsx",
|
|
1633
|
+
contents
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
// src/plugins/use-pages/build/plugins/image-plugin.ts
|
|
1641
|
+
import { createHash as createHash2 } from "crypto";
|
|
1642
|
+
import path6 from "path";
|
|
1643
|
+
import sharp2 from "sharp";
|
|
1644
|
+
import fs4 from "fs/promises";
|
|
1645
|
+
var imagePlugin = {
|
|
1646
|
+
name: "image-plugin",
|
|
1647
|
+
setup(build2) {
|
|
1648
|
+
const outdir = build2.initialOptions.outdir;
|
|
1649
|
+
const publicPath = build2.initialOptions.publicPath;
|
|
1650
|
+
if (!outdir || !publicPath) {
|
|
1651
|
+
throw new Error("outdir and publicPath must be set in esbuild options");
|
|
1652
|
+
}
|
|
1653
|
+
build2.onResolve({ filter: /\.(png|jpe?g)$/ }, async (args) => {
|
|
1654
|
+
const filePath = path6.resolve(args.resolveDir, args.path);
|
|
1655
|
+
const fileName = path6.basename(filePath);
|
|
1656
|
+
const extname = path6.extname(filePath);
|
|
1657
|
+
const hash = createHash2("md5").update(filePath + await fs4.readFile(filePath)).digest("hex").slice(0, 8);
|
|
1658
|
+
const newFilename = `${fileName}-${hash}${extname}`;
|
|
1659
|
+
const newFilePath = path6.join(outdir, "media", newFilename);
|
|
1660
|
+
await fs4.mkdir(path6.dirname(newFilePath), { recursive: true });
|
|
1661
|
+
await fs4.copyFile(filePath, newFilePath);
|
|
1662
|
+
return {
|
|
1663
|
+
path: newFilePath,
|
|
1664
|
+
namespace: "image"
|
|
1665
|
+
};
|
|
1666
|
+
});
|
|
1667
|
+
build2.onLoad({ filter: /\.png$|\.jpg$/ }, async (args) => {
|
|
1668
|
+
const image = sharp2(args.path);
|
|
1669
|
+
const metadata = await image.metadata();
|
|
1670
|
+
const url = `${publicPath}/media/${path6.basename(args.path)}`;
|
|
1671
|
+
const searchParams = new URLSearchParams({});
|
|
1672
|
+
if (metadata.width) {
|
|
1673
|
+
searchParams.set("w", metadata.width.toString());
|
|
1674
|
+
}
|
|
1675
|
+
if (metadata.height) {
|
|
1676
|
+
searchParams.set("h", metadata.height.toString());
|
|
1677
|
+
}
|
|
1678
|
+
const output = image.resize({
|
|
1679
|
+
width: Math.min(metadata.width ?? 16, 16),
|
|
1680
|
+
height: Math.min(metadata.height ?? 16, 16),
|
|
1681
|
+
fit: "inside"
|
|
1682
|
+
}).toFormat("webp", {
|
|
1683
|
+
quality: 20,
|
|
1684
|
+
alphaQuality: 20,
|
|
1685
|
+
smartSubsample: true
|
|
1686
|
+
});
|
|
1687
|
+
const { data, info } = await output.toBuffer({ resolveWithObject: true });
|
|
1688
|
+
const dataURIBase64 = `data:image/${info.format};base64,${data.toString(
|
|
1689
|
+
"base64"
|
|
1690
|
+
)}`;
|
|
1691
|
+
if (dataURIBase64) {
|
|
1692
|
+
searchParams.set("blurDataURL", dataURIBase64);
|
|
1693
|
+
}
|
|
1694
|
+
return {
|
|
1695
|
+
contents: `${url}?${searchParams.toString()}`,
|
|
1696
|
+
loader: "text"
|
|
1697
|
+
};
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
// src/plugins/use-pages/build/plugins/postcss-plugin.ts
|
|
1703
|
+
import fs5 from "fs/promises";
|
|
1704
|
+
import loadConfig from "postcss-load-config";
|
|
1705
|
+
import postcss from "postcss";
|
|
1706
|
+
var postcssPlugin = {
|
|
1707
|
+
name: "postcss-plugin",
|
|
1708
|
+
setup(build2) {
|
|
1709
|
+
build2.onLoad({ filter: /.css$/, namespace: "file" }, async (args) => {
|
|
1710
|
+
const { plugins, options } = await loadConfig();
|
|
1711
|
+
const css = await fs5.readFile(args.path, "utf-8");
|
|
1712
|
+
const result = await postcss(plugins).process(css, {
|
|
1713
|
+
...options,
|
|
1714
|
+
from: args.path
|
|
1715
|
+
}).then((result2) => result2);
|
|
1716
|
+
return {
|
|
1717
|
+
contents: result.css,
|
|
1718
|
+
loader: "css"
|
|
1719
|
+
};
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// src/plugins/use-pages/build/index.ts
|
|
1725
|
+
var DIST_STATIC_DIR = path7.join(process.cwd(), ".pylon/__pylon/static");
|
|
1726
|
+
var DIST_PAGES_DIR = path7.join(process.cwd(), ".pylon/__pylon/pages");
|
|
1727
|
+
async function updateFileIfChanged(path8, newContent) {
|
|
1728
|
+
try {
|
|
1729
|
+
const currentContent = await fs6.readFile(path8, "utf8");
|
|
1730
|
+
if (currentContent === newContent) {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
if (err.code !== "ENOENT") throw err;
|
|
1735
|
+
}
|
|
1736
|
+
await fs6.writeFile(path8, newContent, "utf8");
|
|
1737
|
+
return true;
|
|
1738
|
+
}
|
|
1739
|
+
var build = async () => {
|
|
1740
|
+
const buildAppFile = async () => {
|
|
1741
|
+
const pagesRoutes = await getPageRoutes();
|
|
1742
|
+
const appContent = generateAppFile(pagesRoutes);
|
|
1743
|
+
const appFilePath = path7.resolve(process.cwd(), ".pylon", "app.tsx");
|
|
1744
|
+
const state = await updateFileIfChanged(appFilePath, appContent);
|
|
1745
|
+
if (state) {
|
|
1746
|
+
console.log("Updated app file");
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
const writeOnEndPlugin = {
|
|
1750
|
+
name: "write-on-end",
|
|
1751
|
+
setup(build2) {
|
|
1752
|
+
build2.onEnd(async (result) => {
|
|
1753
|
+
result.outputFiles?.forEach(async (file) => {
|
|
1754
|
+
await fs6.mkdir(path7.dirname(file.path), { recursive: true });
|
|
1755
|
+
await updateFileIfChanged(file.path, file.text);
|
|
1756
|
+
});
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
let pagesWatcher = null;
|
|
1761
|
+
const clientCtx = await esbuild.context({
|
|
1762
|
+
write: false,
|
|
1763
|
+
metafile: true,
|
|
1764
|
+
absWorkingDir: process.cwd(),
|
|
1765
|
+
plugins: [
|
|
1766
|
+
injectAppHydrationPlugin,
|
|
1767
|
+
imagePlugin,
|
|
1768
|
+
postcssPlugin,
|
|
1769
|
+
writeOnEndPlugin
|
|
1770
|
+
],
|
|
1771
|
+
publicPath: "/__pylon/static",
|
|
1772
|
+
assetNames: "assets/[name]-[hash]",
|
|
1773
|
+
chunkNames: "chunks/[name]-[hash]",
|
|
1774
|
+
format: "esm",
|
|
1775
|
+
platform: "browser",
|
|
1776
|
+
entryPoints: [".pylon/app.tsx"],
|
|
1777
|
+
outdir: DIST_STATIC_DIR,
|
|
1778
|
+
bundle: true,
|
|
1779
|
+
splitting: true,
|
|
1780
|
+
minify: false,
|
|
1781
|
+
loader: {
|
|
1782
|
+
// Map file extensions to the file loader
|
|
1783
|
+
".svg": "file",
|
|
1784
|
+
".woff": "file",
|
|
1785
|
+
".woff2": "file"
|
|
1786
|
+
},
|
|
1787
|
+
define: {
|
|
1788
|
+
"process.env.NODE_ENV": '"production"'
|
|
1789
|
+
},
|
|
1790
|
+
mainFields: ["browser", "module", "main"]
|
|
1791
|
+
});
|
|
1792
|
+
const serverCtx = await esbuild.context({
|
|
1793
|
+
write: false,
|
|
1794
|
+
absWorkingDir: process.cwd(),
|
|
1795
|
+
plugins: [imagePlugin, postcssPlugin, writeOnEndPlugin],
|
|
1796
|
+
publicPath: "/__pylon/static",
|
|
1797
|
+
assetNames: "assets/[name]-[hash]",
|
|
1798
|
+
chunkNames: "chunks/[name]-[hash]",
|
|
1799
|
+
format: "esm",
|
|
1800
|
+
platform: "node",
|
|
1801
|
+
entryPoints: [".pylon/app.tsx"],
|
|
1802
|
+
outdir: DIST_PAGES_DIR,
|
|
1803
|
+
bundle: true,
|
|
1804
|
+
packages: "external",
|
|
1805
|
+
splitting: false,
|
|
1806
|
+
minify: false,
|
|
1807
|
+
loader: {
|
|
1808
|
+
// Map file extensions to the file loader
|
|
1809
|
+
".svg": "file",
|
|
1810
|
+
".woff": "file",
|
|
1811
|
+
".woff2": "file"
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
return {
|
|
1815
|
+
watch: async () => {
|
|
1816
|
+
console.log("Watching pages directory...");
|
|
1817
|
+
pagesWatcher = chokidar.watch("pages", { ignoreInitial: false });
|
|
1818
|
+
pagesWatcher.on("all", (event, path8) => {
|
|
1819
|
+
if (["add", "change", "unlink"].includes(event)) {
|
|
1820
|
+
buildAppFile();
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
await Promise.all([clientCtx.watch(), serverCtx.watch()]);
|
|
1824
|
+
},
|
|
1825
|
+
dispose: async () => {
|
|
1826
|
+
console.log("Disposing pages");
|
|
1827
|
+
if (pagesWatcher) {
|
|
1828
|
+
pagesWatcher.close();
|
|
1829
|
+
}
|
|
1830
|
+
Promise.all([clientCtx.dispose(), serverCtx.dispose()]);
|
|
1831
|
+
},
|
|
1832
|
+
rebuild: async () => {
|
|
1833
|
+
console.log("Rebuilding pages");
|
|
1834
|
+
await buildAppFile();
|
|
1835
|
+
await Promise.all([clientCtx.rebuild(), serverCtx.rebuild()]);
|
|
1836
|
+
return {};
|
|
1837
|
+
},
|
|
1838
|
+
cancel: async () => {
|
|
1839
|
+
if (pagesWatcher) {
|
|
1840
|
+
await pagesWatcher.close();
|
|
1841
|
+
}
|
|
1842
|
+
await Promise.all([clientCtx.cancel(), serverCtx.cancel()]);
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
// src/plugins/use-pages/index.ts
|
|
1848
|
+
function usePages() {
|
|
1849
|
+
return {
|
|
1850
|
+
setup,
|
|
1851
|
+
build
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1159
1854
|
export {
|
|
1160
1855
|
ServiceError,
|
|
1161
1856
|
app,
|
|
1162
1857
|
asyncContext,
|
|
1163
|
-
|
|
1858
|
+
authMiddleware,
|
|
1164
1859
|
createDecorator,
|
|
1165
1860
|
createPubSub as experimentalCreatePubSub,
|
|
1166
1861
|
getContext,
|
|
1167
1862
|
getEnv,
|
|
1168
1863
|
handler,
|
|
1169
1864
|
requireAuth,
|
|
1170
|
-
setContext
|
|
1865
|
+
setContext,
|
|
1866
|
+
useAuth,
|
|
1867
|
+
usePages
|
|
1171
1868
|
};
|
|
1172
1869
|
//# sourceMappingURL=index.js.map
|