shraga 0.1.3 → 0.1.5
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/README.md +115 -3
- package/defaults/extensions/stripe-webhook.ext.ts +29 -21
- package/dist/client/assets/{index-BoHttkMt.js → index-BnArwb7g.js} +43 -43
- package/dist/client/index.html +1 -1
- package/package.json +7 -4
- package/src/cli.ts +3 -1
- package/src/client/components/ArtifactPanel.tsx +16 -3
- package/src/index.ts +180 -0
- package/src/server/artifacts/artifacts.routes.ts +2 -19
- package/src/server/boot.ts +1815 -0
- package/src/server/events/bus.ts +29 -5
- package/src/server/events/types.ts +26 -3
- package/src/server/events/webhook.ts +64 -0
- package/src/server/extensions.ts +48 -0
- package/src/server/index.ts +8 -1712
- package/src/server/spa-catchall.ts +41 -0
- package/src/server/artifacts/artifacts.export.ts +0 -85
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import type express from 'express';
|
|
4
|
+
|
|
5
|
+
// Non-page prefixes that must fall through to a real 404 (JSON/API/transport), never the SPA shell.
|
|
6
|
+
function isNonPagePath(p: string): boolean {
|
|
7
|
+
return p.startsWith('/api/') || p.startsWith('/mcp') || p.startsWith('/uploads') ||
|
|
8
|
+
p.startsWith('/internal/') || p.startsWith('/.well-known');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register the SPA catch-all (`app.get('*')`) as the LAST GET route so it serves the built
|
|
13
|
+
* index.html for client routes/deep links (/oauth/authorize, /cli-auth, /session/x, …) while
|
|
14
|
+
* letting unmatched API/MCP/upload/internal/oauth-metadata paths fall through to a real 404.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent + re-placeable: each call splices out any prior catch-all layer, so it can be called
|
|
17
|
+
* again after a later mountFeatures() (passive→active promotion) and still sit behind those routes.
|
|
18
|
+
* In dev (no built dist) it is a no-op — Vite serves the SPA and a catch-all would 404-shadow it.
|
|
19
|
+
*
|
|
20
|
+
* EXPRESS 4 COUPLING: uses the bare `app.get('*')` route and Express's private `_router.stack`.
|
|
21
|
+
* A future express@5 bump breaks this LOUDLY (path-to-regexp v8 rejects `'*'`), not silently —
|
|
22
|
+
* re-verify the promotion-path ordering if express is ever upgraded.
|
|
23
|
+
*/
|
|
24
|
+
export function registerSpaCatchAll(app: express.Express, distPath: string): void {
|
|
25
|
+
if (!existsSync(distPath)) return;
|
|
26
|
+
const indexHtml = path.join(distPath, 'index.html');
|
|
27
|
+
// For app.get(), the layer's `handle` is the Route's dispatcher — our tagged handler lives inside
|
|
28
|
+
// `layer.route.stack[*].handle`. Match on that so re-registration removes the stale catch-all.
|
|
29
|
+
const stack = (app as any)._router?.stack as Array<{ route?: { stack?: Array<{ handle?: { __spaCatchAll?: boolean } }> } }> | undefined;
|
|
30
|
+
if (stack) {
|
|
31
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
32
|
+
if (stack[i]?.route?.stack?.some((h) => h?.handle?.__spaCatchAll)) stack.splice(i, 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const handler: express.RequestHandler = (req, res, next) => {
|
|
36
|
+
if (isNonPagePath(req.path)) return next();
|
|
37
|
+
res.sendFile(indexHtml);
|
|
38
|
+
};
|
|
39
|
+
(handler as { __spaCatchAll?: boolean }).__spaCatchAll = true;
|
|
40
|
+
app.get('*', handler);
|
|
41
|
+
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { dataPath } from '../paths.ts';
|
|
4
|
-
|
|
5
|
-
const PREFIX = '[artifacts:export]';
|
|
6
|
-
|
|
7
|
-
let browserInstance: any = null;
|
|
8
|
-
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
9
|
-
const IDLE_TIMEOUT = 60_000;
|
|
10
|
-
|
|
11
|
-
async function getBrowser() {
|
|
12
|
-
if (browserInstance) {
|
|
13
|
-
resetIdleTimer();
|
|
14
|
-
return browserInstance;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let puppeteer: any;
|
|
18
|
-
try {
|
|
19
|
-
puppeteer = await import('puppeteer');
|
|
20
|
-
} catch {
|
|
21
|
-
throw new Error('puppeteer not installed. Run: bun add puppeteer');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
console.log(PREFIX, 'launching browser');
|
|
25
|
-
browserInstance = await puppeteer.default.launch({
|
|
26
|
-
headless: true,
|
|
27
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
browserInstance.on('disconnected', () => { browserInstance = null; });
|
|
31
|
-
resetIdleTimer();
|
|
32
|
-
return browserInstance;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function resetIdleTimer() {
|
|
36
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
37
|
-
idleTimer = setTimeout(async () => {
|
|
38
|
-
if (browserInstance) {
|
|
39
|
-
console.log(PREFIX, 'closing idle browser');
|
|
40
|
-
await browserInstance.close().catch(() => {});
|
|
41
|
-
browserInstance = null;
|
|
42
|
-
}
|
|
43
|
-
}, IDLE_TIMEOUT);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function exportToPng(
|
|
47
|
-
html: string,
|
|
48
|
-
dimensions: [number, number],
|
|
49
|
-
): Promise<Buffer> {
|
|
50
|
-
const [width, height] = dimensions;
|
|
51
|
-
const browser = await getBrowser();
|
|
52
|
-
const page = await browser.newPage();
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
await page.setViewport({ width, height, deviceScaleFactor: 2 });
|
|
56
|
-
await page.setContent(html, { waitUntil: 'networkidle0', timeout: 15_000 });
|
|
57
|
-
const png = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width, height } });
|
|
58
|
-
return Buffer.from(png);
|
|
59
|
-
} finally {
|
|
60
|
-
await page.close();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function exportAndSave(
|
|
65
|
-
sessionId: string,
|
|
66
|
-
artifactId: string,
|
|
67
|
-
html: string,
|
|
68
|
-
dimensions: [number, number],
|
|
69
|
-
): Promise<string> {
|
|
70
|
-
const png = await exportToPng(html, dimensions);
|
|
71
|
-
const dir = dataPath(`sessions/${sessionId}/artifacts`);
|
|
72
|
-
mkdirSync(dir, { recursive: true });
|
|
73
|
-
const filePath = path.join(dir, `${artifactId}.png`);
|
|
74
|
-
writeFileSync(filePath, png);
|
|
75
|
-
console.log(PREFIX, `saved ${filePath} (${png.length} bytes)`);
|
|
76
|
-
return filePath;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function closeBrowser() {
|
|
80
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
81
|
-
if (browserInstance) {
|
|
82
|
-
await browserInstance.close().catch(() => {});
|
|
83
|
-
browserInstance = null;
|
|
84
|
-
}
|
|
85
|
-
}
|