@kahitsan/plugin-sdk 0.2.0 → 0.3.0-staging.43
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/bin/cli.mjs +54 -0
- package/dist/index.js +1 -1
- package/package.json +8 -2
- package/shell/_build/.vite/manifest.json +189 -0
- package/shell/_build/.vite/manifest.json.br +0 -0
- package/shell/_build/.vite/manifest.json.gz +0 -0
- package/shell/_build/assets/_...slug_-BNycC89F.js +1 -0
- package/shell/_build/assets/_...slug_-BNycC89F.js.br +0 -0
- package/shell/_build/assets/_...slug_-BNycC89F.js.gz +0 -0
- package/shell/_build/assets/api-keys-zDu0DTIi.js +1 -0
- package/shell/_build/assets/auth-CPW4mGTx.js +70 -0
- package/shell/_build/assets/auth-CPW4mGTx.js.br +0 -0
- package/shell/_build/assets/auth-CPW4mGTx.js.gz +0 -0
- package/shell/_build/assets/client-B0tH7z8C.css +1 -0
- package/shell/_build/assets/client-B0tH7z8C.css.br +0 -0
- package/shell/_build/assets/client-B0tH7z8C.css.gz +0 -0
- package/shell/_build/assets/client-BhDpDGXU.js +2 -0
- package/shell/_build/assets/client-BhDpDGXU.js.br +0 -0
- package/shell/_build/assets/client-BhDpDGXU.js.gz +0 -0
- package/shell/_build/assets/components-kMeatXhB.js +1 -0
- package/shell/_build/assets/dashboard-BwMkrsX2.js +1 -0
- package/shell/_build/assets/index-dCQOOcel.js +1 -0
- package/shell/_build/assets/login-BwMkrsX2.js +1 -0
- package/shell/_build/assets/members-CKVe_2mI.js +1 -0
- package/shell/_build/assets/plugin-ui-rgxz3UHD.js +10 -0
- package/shell/_build/assets/plugin-ui-rgxz3UHD.js.br +0 -0
- package/shell/_build/assets/plugin-ui-rgxz3UHD.js.gz +0 -0
- package/shell/_build/assets/register-BwMkrsX2.js +1 -0
- package/shell/_build/assets/routing-BlHUL5zP.js +1 -0
- package/shell/_build/assets/routing-BlHUL5zP.js.br +0 -0
- package/shell/_build/assets/routing-BlHUL5zP.js.gz +0 -0
- package/shell/_build/assets/settings-BwMkrsX2.js +1 -0
- package/shell/_build/assets/users-BwMkrsX2.js +1 -0
- package/shell/_build/assets/workspace-nav-DsOJmRdY.js +1 -0
- package/shell/_build/assets/workspace-nav-DsOJmRdY.js.br +0 -0
- package/shell/_build/assets/workspace-nav-DsOJmRdY.js.gz +0 -0
- package/shell/_build/assets/workspace-settings-BwMkrsX2.js +1 -0
- package/shell/_build/assets/workspaces-BwMkrsX2.js +1 -0
- package/shell/_headers +20 -0
- package/shell/_redirects +4 -0
- package/shell/_server/assets/app-B0tH7z8C.css +1 -0
- package/shell/_server/assets/app-B0tH7z8C.css.br +0 -0
- package/shell/_server/assets/app-B0tH7z8C.css.gz +0 -0
- package/shell/apple-touch-icon.png +0 -0
- package/shell/dev/apple-touch-icon.png +0 -0
- package/shell/dev/favicon-32.png +0 -0
- package/shell/dev/favicon.ico +0 -0
- package/shell/dev/manifest.webmanifest +31 -0
- package/shell/dev/pwa-192.png +0 -0
- package/shell/dev/pwa-512-maskable.png +0 -0
- package/shell/dev/pwa-512.png +0 -0
- package/shell/favicon-32.png +0 -0
- package/shell/favicon.ico +0 -0
- package/shell/index.html +1 -0
- package/shell/index.html.br +0 -0
- package/shell/index.html.gz +0 -0
- package/shell/manifest.webmanifest +31 -0
- package/shell/pwa-192.png +0 -0
- package/shell/pwa-512-maskable.png +0 -0
- package/shell/pwa-512.png +0 -0
- package/shell/sw.js +103 -0
- package/shell/sw.js.br +0 -0
- package/shell/sw.js.gz +0 -0
- package/src/dev/dev-assets.ts +156 -0
- package/src/dev/dev-host.ts +194 -0
- package/src/dev/dev-kernel.ts +499 -0
- package/src/dev/preload.ts +61 -0
- package/src/dev/serve.ts +141 -0
- package/src/dev/sqlite-pool.ts +358 -0
package/shell/index.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#0a0a0a"><link rel="icon" href="/favicon.ico" sizes="any"><link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32"><link rel="manifest" href="/manifest.webmanifest"><link rel="apple-touch-icon" href="/apple-touch-icon.png"><link href="/_build/assets/client-B0tH7z8C.css" rel="stylesheet" fetchPriority="high" /><link href="/_build/assets/routing-BlHUL5zP.js" rel="modulepreload" /><link href="/_build/assets/components-kMeatXhB.js" rel="modulepreload" /><link href="/_build/assets/auth-CPW4mGTx.js" rel="modulepreload" /><link href="/_build/assets/plugin-ui-rgxz3UHD.js" rel="modulepreload" /><link href="/_build/assets/workspace-nav-DsOJmRdY.js" rel="modulepreload" /><link href="/_build/assets/client-BhDpDGXU.js" rel="modulepreload" /></head><body><div id="app"></div><!--$--><script>window.manifest = {"src/routes/[...slug].tsx?pick=default&pick=$css":{"output":"/_build/assets/_...slug_-BNycC89F.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/auth-CPW4mGTx.js","key":"/_build/assets/auth-CPW4mGTx.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/plugin-ui-rgxz3UHD.js","key":"/_build/assets/plugin-ui-rgxz3UHD.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/_...slug_-BNycC89F.js","key":"/_build/assets/_...slug_-BNycC89F.js","rel":"modulepreload"}}]},"src/routes/api-keys.tsx?pick=default&pick=$css":{"output":"/_build/assets/api-keys-zDu0DTIi.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/api-keys-zDu0DTIi.js","key":"/_build/assets/api-keys-zDu0DTIi.js","rel":"modulepreload"}}]},"src/routes/dashboard.tsx?pick=thisWeekToken&pick=thisYearToken&pick=default&pick=$css":{"output":"/_build/assets/dashboard-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/dashboard-BwMkrsX2.js","key":"/_build/assets/dashboard-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/index.tsx?pick=default&pick=$css":{"output":"/_build/assets/index-dCQOOcel.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/auth-CPW4mGTx.js","key":"/_build/assets/auth-CPW4mGTx.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspace-nav-DsOJmRdY.js","key":"/_build/assets/workspace-nav-DsOJmRdY.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/index-dCQOOcel.js","key":"/_build/assets/index-dCQOOcel.js","rel":"modulepreload"}}]},"src/routes/login.tsx?pick=default&pick=$css":{"output":"/_build/assets/login-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/login-BwMkrsX2.js","key":"/_build/assets/login-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/members.tsx?pick=default&pick=$css":{"output":"/_build/assets/members-CKVe_2mI.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/members-CKVe_2mI.js","key":"/_build/assets/members-CKVe_2mI.js","rel":"modulepreload"}}]},"src/routes/register.tsx?pick=default&pick=$css":{"output":"/_build/assets/register-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/register-BwMkrsX2.js","key":"/_build/assets/register-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/settings.tsx?pick=default&pick=$css":{"output":"/_build/assets/settings-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/settings-BwMkrsX2.js","key":"/_build/assets/settings-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/users.tsx?pick=default&pick=$css":{"output":"/_build/assets/users-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/users-BwMkrsX2.js","key":"/_build/assets/users-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/workspace-settings.tsx?pick=default&pick=$css":{"output":"/_build/assets/workspace-settings-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspace-settings-BwMkrsX2.js","key":"/_build/assets/workspace-settings-BwMkrsX2.js","rel":"modulepreload"}}]},"src/routes/workspaces.tsx?pick=default&pick=$css":{"output":"/_build/assets/workspaces-BwMkrsX2.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspaces-BwMkrsX2.js","key":"/_build/assets/workspaces-BwMkrsX2.js","rel":"modulepreload"}}]},"virtual:$vinxi/handler/client":{"output":"/_build/assets/client-BhDpDGXU.js","assets":[{"tag":"link","attrs":{"href":"/_build/assets/client-B0tH7z8C.css","key":"/_build/assets/client-B0tH7z8C.css","rel":"stylesheet","fetchPriority":"high"}},{"tag":"link","attrs":{"href":"/_build/assets/routing-BlHUL5zP.js","key":"/_build/assets/routing-BlHUL5zP.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/components-kMeatXhB.js","key":"/_build/assets/components-kMeatXhB.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/auth-CPW4mGTx.js","key":"/_build/assets/auth-CPW4mGTx.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/plugin-ui-rgxz3UHD.js","key":"/_build/assets/plugin-ui-rgxz3UHD.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/workspace-nav-DsOJmRdY.js","key":"/_build/assets/workspace-nav-DsOJmRdY.js","rel":"modulepreload"}},{"tag":"link","attrs":{"href":"/_build/assets/client-BhDpDGXU.js","key":"/_build/assets/client-BhDpDGXU.js","rel":"modulepreload"}}]}}</script><script type="module" src="/_build/assets/client-BhDpDGXU.js"></script><!--/--></body></html>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Hilinga",
|
|
3
|
+
"short_name": "Hilinga",
|
|
4
|
+
"description": "Hilinga by Kahitsan Corp. — clients, transactions, packages, vouchers, and timesheets.",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"scope": "/",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"orientation": "any",
|
|
9
|
+
"background_color": "#0a0a0a",
|
|
10
|
+
"theme_color": "#0a0a0a",
|
|
11
|
+
"icons": [
|
|
12
|
+
{
|
|
13
|
+
"src": "/pwa-192.png",
|
|
14
|
+
"sizes": "192x192",
|
|
15
|
+
"type": "image/png",
|
|
16
|
+
"purpose": "any"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"src": "/pwa-512.png",
|
|
20
|
+
"sizes": "512x512",
|
|
21
|
+
"type": "image/png",
|
|
22
|
+
"purpose": "any"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"src": "/pwa-512-maskable.png",
|
|
26
|
+
"sizes": "512x512",
|
|
27
|
+
"type": "image/png",
|
|
28
|
+
"purpose": "maskable"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/shell/sw.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* ksERP service worker — generated at build time. Do not edit by hand. */
|
|
2
|
+
/* The build replaces 0.76.3 with the value from package.json. */
|
|
3
|
+
/* eslint-env serviceworker */
|
|
4
|
+
|
|
5
|
+
const APP_VERSION = "0.76.3";
|
|
6
|
+
const CACHE_NAME = "ks-erp-v" + APP_VERSION;
|
|
7
|
+
|
|
8
|
+
// App shell — the bare-minimum entrypoints needed to render an offline page.
|
|
9
|
+
// We keep this short because the SPA's hashed chunks can't all be listed; the
|
|
10
|
+
// fetch handler caches them as the user visits them, with network-first.
|
|
11
|
+
const PRECACHE_URLS = ["/", "/manifest.webmanifest"];
|
|
12
|
+
|
|
13
|
+
self.addEventListener("install", (event) => {
|
|
14
|
+
event.waitUntil(
|
|
15
|
+
caches.open(CACHE_NAME).then((cache) => {
|
|
16
|
+
return cache.addAll(PRECACHE_URLS).catch(() => {
|
|
17
|
+
// Best-effort: if any URL 404s in dev/preview, don't block install.
|
|
18
|
+
});
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
// Do NOT skipWaiting() here. We want updates to wait until the user clicks
|
|
22
|
+
// Reload in the PwaUpdatePrompt banner. The user-driven flow lives in the
|
|
23
|
+
// message handler below (SKIP_WAITING). On a fresh install with no
|
|
24
|
+
// previous controller, the worker activates immediately anyway (nothing
|
|
25
|
+
// to wait for), so this only changes behavior on the update path.
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
self.addEventListener("activate", (event) => {
|
|
29
|
+
event.waitUntil(
|
|
30
|
+
(async () => {
|
|
31
|
+
const keys = await caches.keys();
|
|
32
|
+
await Promise.all(
|
|
33
|
+
keys
|
|
34
|
+
.filter((key) => key.startsWith("ks-erp-v") && key !== CACHE_NAME)
|
|
35
|
+
.map((key) => caches.delete(key)),
|
|
36
|
+
);
|
|
37
|
+
await self.clients.claim();
|
|
38
|
+
})(),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
self.addEventListener("fetch", (event) => {
|
|
43
|
+
const req = event.request;
|
|
44
|
+
|
|
45
|
+
// Only handle GETs. Don't touch /api/* — let those go straight to the network.
|
|
46
|
+
if (req.method !== "GET") return;
|
|
47
|
+
|
|
48
|
+
const url = new URL(req.url);
|
|
49
|
+
if (url.origin !== self.location.origin) return;
|
|
50
|
+
if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/uploads/")) return;
|
|
51
|
+
|
|
52
|
+
// Network-first for navigations: fresh deploys aren't masked by stale shell.
|
|
53
|
+
if (req.mode === "navigate") {
|
|
54
|
+
event.respondWith(
|
|
55
|
+
fetch(req)
|
|
56
|
+
.then((res) => {
|
|
57
|
+
const copy = res.clone();
|
|
58
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {});
|
|
59
|
+
return res;
|
|
60
|
+
})
|
|
61
|
+
.catch(async () => {
|
|
62
|
+
const cached = await caches.match(req);
|
|
63
|
+
if (cached) return cached;
|
|
64
|
+
const fallback = await caches.match("/");
|
|
65
|
+
return (
|
|
66
|
+
fallback ||
|
|
67
|
+
new Response("You are offline.", { status: 503, headers: { "Content-Type": "text/plain" } })
|
|
68
|
+
);
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Static assets (JS/CSS/icons): stale-while-revalidate.
|
|
75
|
+
event.respondWith(
|
|
76
|
+
caches.match(req).then((cached) => {
|
|
77
|
+
const networkFetch = fetch(req)
|
|
78
|
+
.then((res) => {
|
|
79
|
+
if (res && res.ok) {
|
|
80
|
+
const copy = res.clone();
|
|
81
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
return res;
|
|
84
|
+
})
|
|
85
|
+
.catch(
|
|
86
|
+
() =>
|
|
87
|
+
cached ||
|
|
88
|
+
new Response("Offline", {
|
|
89
|
+
status: 503,
|
|
90
|
+
headers: { "Content-Type": "text/plain" },
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
return cached || networkFetch;
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Allow the page to ask the SW to take over immediately after an update.
|
|
99
|
+
self.addEventListener("message", (event) => {
|
|
100
|
+
if (event.data && event.data.type === "SKIP_WAITING") {
|
|
101
|
+
self.skipWaiting();
|
|
102
|
+
}
|
|
103
|
+
});
|
package/shell/sw.js.br
ADDED
|
Binary file
|
package/shell/sw.js.gz
ADDED
|
Binary file
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Dev-stub asset backend (Phase 5 K3, Vision Data-Arch §8) — the toolkit's stand-in for
|
|
2
|
+
// the kernel's `/_internal/assets/*` endpoints (server/routes/internal-assets.ts). A plugin
|
|
3
|
+
// built with `createPluginServer({ assets: true })` relays `api.assets.{upload,presign,
|
|
4
|
+
// delete}` to KSERP_KERNEL_URL (the dev-kernel) exactly as in prod; this module answers that
|
|
5
|
+
// SAME contract against LOCAL MinIO + an in-memory ledger, clamped to the active view's
|
|
6
|
+
// workspace + owner — so a toolkit dev can upload AND read a file back (the Gate-5 asset
|
|
7
|
+
// requirement) and the ownership clamp behaves like prod (an asset from another workspace/
|
|
8
|
+
// owner is a 404). Bun-only (matches the dev-kernel); never in prod.
|
|
9
|
+
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
// The slice of Bun.S3Client this file uses — mirrors packages/plugin-server-utils/src/s3.ts
|
|
13
|
+
// so the dev path stores objects identically to prod, just pointed at local MinIO.
|
|
14
|
+
interface BunS3File {
|
|
15
|
+
delete(): Promise<number>;
|
|
16
|
+
}
|
|
17
|
+
interface BunS3Client {
|
|
18
|
+
write(key: string, data: Uint8Array, opts?: { type?: string; acl?: string }): Promise<number>;
|
|
19
|
+
file(key: string): BunS3File;
|
|
20
|
+
presign(key: string, opts?: { method?: string; expiresIn?: number }): string;
|
|
21
|
+
}
|
|
22
|
+
interface BunRuntime {
|
|
23
|
+
S3Client: new (o: {
|
|
24
|
+
accessKeyId: string;
|
|
25
|
+
secretAccessKey: string;
|
|
26
|
+
bucket: string;
|
|
27
|
+
region: string;
|
|
28
|
+
endpoint: string;
|
|
29
|
+
}) => BunS3Client;
|
|
30
|
+
}
|
|
31
|
+
const BUN = (globalThis as { Bun?: BunRuntime }).Bun;
|
|
32
|
+
|
|
33
|
+
/** A Bun S3 client for local MinIO, or null when S3_* isn't configured (→ 503, like prod). */
|
|
34
|
+
function devS3(): BunS3Client | null {
|
|
35
|
+
const { S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY } = process.env;
|
|
36
|
+
if (!BUN || !S3_ENDPOINT || !S3_BUCKET || !S3_ACCESS_KEY || !S3_SECRET_KEY) return null;
|
|
37
|
+
return new BUN.S3Client({
|
|
38
|
+
accessKeyId: S3_ACCESS_KEY,
|
|
39
|
+
secretAccessKey: S3_SECRET_KEY,
|
|
40
|
+
bucket: S3_BUCKET,
|
|
41
|
+
region: S3_REGION || "us-east-1",
|
|
42
|
+
endpoint: new URL(S3_ENDPOINT).origin,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface DevAsset {
|
|
47
|
+
objectKey: string;
|
|
48
|
+
workspaceId: number;
|
|
49
|
+
ownerId: string;
|
|
50
|
+
fileName: string;
|
|
51
|
+
fileSize: number;
|
|
52
|
+
mimeType: string;
|
|
53
|
+
}
|
|
54
|
+
// The in-memory equivalent of the prod `kernel_assets` ledger — a single local dev session,
|
|
55
|
+
// so a process-global Map is enough. Cleared on dev-kernel restart (matches "no seeding").
|
|
56
|
+
const ledger = new Map<number, DevAsset>();
|
|
57
|
+
let nextId = 1;
|
|
58
|
+
|
|
59
|
+
const PRESIGN_TTL_SECONDS = 3600;
|
|
60
|
+
|
|
61
|
+
function json(body: unknown, status = 200): Response {
|
|
62
|
+
return Response.json(body, { status });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A ledger row, but ONLY if it belongs to the active view's workspace + owner — mirrors
|
|
66
|
+
* the prod `ownedObjectKey` clamp so a wrong-workspace/owner id is indistinguishable from
|
|
67
|
+
* absent (404). */
|
|
68
|
+
function owned(
|
|
69
|
+
assetId: unknown,
|
|
70
|
+
workspaceId: number,
|
|
71
|
+
ownerId: string,
|
|
72
|
+
): { id: number; row: DevAsset } | null {
|
|
73
|
+
const id = Number(assetId);
|
|
74
|
+
if (!Number.isInteger(id) || id <= 0) return null;
|
|
75
|
+
const row = ledger.get(id);
|
|
76
|
+
if (!row || row.workspaceId !== workspaceId || row.ownerId !== ownerId) return null;
|
|
77
|
+
return { id, row };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface DevIdentity {
|
|
81
|
+
workspaceId: number;
|
|
82
|
+
ownerId: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function doUpload(req: Request, id: DevIdentity, s3: BunS3Client): Promise<Response> {
|
|
86
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
87
|
+
pluginId?: string;
|
|
88
|
+
fileName?: string;
|
|
89
|
+
mimeType?: string;
|
|
90
|
+
data?: string;
|
|
91
|
+
};
|
|
92
|
+
const pluginId = String(body.pluginId ?? "");
|
|
93
|
+
const fileName = String(body.fileName ?? "");
|
|
94
|
+
const mimeType = String(body.mimeType ?? "");
|
|
95
|
+
if (!pluginId || !fileName || !mimeType || typeof body.data !== "string") {
|
|
96
|
+
return json({ error: "pluginId, fileName, mimeType, data required" }, 400);
|
|
97
|
+
}
|
|
98
|
+
const buf = Buffer.from(body.data, "base64");
|
|
99
|
+
// Kernel-built key ({ws}/{plugin}/{rand}), never caller-supplied — a plugin can't write
|
|
100
|
+
// outside its own prefix, same invariant as prod.
|
|
101
|
+
const objectKey = `${id.workspaceId}/${pluginId}/${randomUUID()}`;
|
|
102
|
+
await s3.write(objectKey, new Uint8Array(buf), { type: mimeType, acl: "private" });
|
|
103
|
+
const assetId = nextId++;
|
|
104
|
+
ledger.set(assetId, {
|
|
105
|
+
objectKey,
|
|
106
|
+
workspaceId: id.workspaceId,
|
|
107
|
+
ownerId: id.ownerId,
|
|
108
|
+
fileName,
|
|
109
|
+
fileSize: buf.length,
|
|
110
|
+
mimeType,
|
|
111
|
+
});
|
|
112
|
+
return json({ id: assetId, objectKey, fileName, fileSize: buf.length, mimeType });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function doPresign(req: Request, id: DevIdentity, s3: BunS3Client): Promise<Response> {
|
|
116
|
+
const body = (await req.json().catch(() => ({}))) as { assetId?: number; expirySeconds?: number };
|
|
117
|
+
const hit = owned(body.assetId, id.workspaceId, id.ownerId);
|
|
118
|
+
if (!hit) return json({ error: "asset not found" }, 404);
|
|
119
|
+
const ttl =
|
|
120
|
+
Number.isInteger(body.expirySeconds) && (body.expirySeconds as number) > 0
|
|
121
|
+
? (body.expirySeconds as number)
|
|
122
|
+
: PRESIGN_TTL_SECONDS;
|
|
123
|
+
const presignedUrl = s3.presign(hit.row.objectKey, { method: "GET", expiresIn: ttl });
|
|
124
|
+
const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
|
|
125
|
+
return json({ presignedUrl, expiresAt });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function doDelete(req: Request, id: DevIdentity, s3: BunS3Client): Promise<Response> {
|
|
129
|
+
const body = (await req.json().catch(() => ({}))) as { assetId?: number };
|
|
130
|
+
const hit = owned(body.assetId, id.workspaceId, id.ownerId);
|
|
131
|
+
if (!hit) return json({ error: "asset not found" }, 404);
|
|
132
|
+
await s3.file(hit.row.objectKey).delete();
|
|
133
|
+
ledger.delete(hit.id);
|
|
134
|
+
return json({});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Answer the `/_internal/assets/*` contract for the dev-kernel, clamped to the supplied
|
|
139
|
+
* active-view identity. Returns null for a non-asset path (caller falls through to its
|
|
140
|
+
* other routes).
|
|
141
|
+
*/
|
|
142
|
+
export async function handleDevAsset(
|
|
143
|
+
path: string,
|
|
144
|
+
req: Request,
|
|
145
|
+
identity: DevIdentity,
|
|
146
|
+
): Promise<Response | null> {
|
|
147
|
+
if (!path.startsWith("/_internal/assets/")) return null;
|
|
148
|
+
const s3 = devS3();
|
|
149
|
+
if (!s3) return json({ error: "asset storage not configured (set S3_* for local MinIO)" }, 503);
|
|
150
|
+
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
151
|
+
|
|
152
|
+
if (path === "/_internal/assets/upload") return doUpload(req, identity, s3);
|
|
153
|
+
if (path === "/_internal/assets/presign") return doPresign(req, identity, s3);
|
|
154
|
+
if (path === "/_internal/assets/delete") return doDelete(req, identity, s3);
|
|
155
|
+
return json({ error: "unknown asset endpoint" }, 404);
|
|
156
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// The dev host (Vision §5.1, K3) — the local environment a plugin developer runs.
|
|
2
|
+
// Boots ONE OR MORE plugins on Bun, each on its own SQLite + synthetic logged-in
|
|
3
|
+
// admin (via the preload), behind a tiny "dev kernel" mediator that stands in for
|
|
4
|
+
// the production kernel's `/_internal/rpc` gateway so cross-plugin calls work — and
|
|
5
|
+
// **degrade gracefully** when a peer isn't loaded (the resilience rule: a missing
|
|
6
|
+
// dependency shows an error, it never crashes the app).
|
|
7
|
+
//
|
|
8
|
+
// bun packages/plugin-sdk/src/dev/dev-host.ts <pluginDir> [<pluginDir> …]
|
|
9
|
+
//
|
|
10
|
+
// Each plugin's own `server/main.ts` is byte-identical to prod — it just finds the
|
|
11
|
+
// dev hooks installed by the preload and runs on the local engine. Loading several
|
|
12
|
+
// at once lets a developer exercise the CONTRACT between plugins (does B expose the
|
|
13
|
+
// method A consumes?) entirely locally, no kernel, no Postgres.
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { spawn, type Subprocess } from "bun";
|
|
18
|
+
import { handleDevAsset } from "./dev-assets.js";
|
|
19
|
+
|
|
20
|
+
const DEV_SECRET = "dev-stub-internal-secret";
|
|
21
|
+
const BASE_PORT = Number(process.env.KSERP_DEV_BASE_PORT || "4500");
|
|
22
|
+
const PRELOAD = join(import.meta.dir, "preload.ts");
|
|
23
|
+
|
|
24
|
+
interface Manifest {
|
|
25
|
+
name: string;
|
|
26
|
+
schemas?: string[];
|
|
27
|
+
}
|
|
28
|
+
interface Loaded {
|
|
29
|
+
dir: string;
|
|
30
|
+
name: string;
|
|
31
|
+
port: number;
|
|
32
|
+
proc: Subprocess;
|
|
33
|
+
status: "starting" | "up" | "failed";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dirs = process.argv.slice(2).map((d) => resolve(d));
|
|
37
|
+
if (dirs.length === 0) {
|
|
38
|
+
console.error("usage: bun dev-host.ts <pluginDir> [<pluginDir> …]");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── assign ports + read manifests (the mediator routes by plugin name → port) ──
|
|
43
|
+
const mediatorPort = BASE_PORT;
|
|
44
|
+
const registry = new Map<string, number>();
|
|
45
|
+
const loaded: Loaded[] = [];
|
|
46
|
+
dirs.forEach((dir, i) => {
|
|
47
|
+
const manifest = JSON.parse(readFileSync(join(dir, "plugin.manifest.json"), "utf8")) as Manifest;
|
|
48
|
+
const port = BASE_PORT + 1 + i;
|
|
49
|
+
registry.set(manifest.name, port);
|
|
50
|
+
loaded.push({ dir, name: manifest.name, port, proc: undefined as never, status: "starting" });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Answer the api.assets relay (`/_internal/assets/*`) against local MinIO + an in-memory
|
|
54
|
+
// ledger, clamped to the active (synthetic) identity the relay forwarded in x-kserp-identity.
|
|
55
|
+
// Returns null for a non-asset path. Extracted from the mediator's fetch to keep that handler
|
|
56
|
+
// within its complexity budget.
|
|
57
|
+
async function tryAssetRelay(pathname: string, req: Request): Promise<Response | null> {
|
|
58
|
+
if (!pathname.startsWith("/_internal/assets/")) return null;
|
|
59
|
+
let identity = { workspaceId: 1, ownerId: "dev-admin" };
|
|
60
|
+
const idHdr = req.headers.get("x-kserp-identity");
|
|
61
|
+
if (idHdr) {
|
|
62
|
+
try {
|
|
63
|
+
const v = JSON.parse(idHdr) as { workspaceId?: number; userId?: string };
|
|
64
|
+
identity = {
|
|
65
|
+
workspaceId: Number(v.workspaceId) || 1,
|
|
66
|
+
ownerId: String(v.userId ?? "dev-admin"),
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
/* malformed → fall back to the default synthetic admin */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return handleDevAsset(pathname, req, identity);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── the dev-kernel mediator: stands in for the prod kernel's /_internal/rpc. Routes
|
|
76
|
+
// a cross-plugin call to the target's /_internal/services/<method>; a peer that
|
|
77
|
+
// isn't loaded → 503, which the SDK's tryCallPlugin turns into a graceful null and
|
|
78
|
+
// callPlugin into a thrown error the consumer's route reports (never a crash). ──
|
|
79
|
+
const mediator = Bun.serve({
|
|
80
|
+
port: mediatorPort,
|
|
81
|
+
async fetch(req) {
|
|
82
|
+
const url = new URL(req.url);
|
|
83
|
+
if (url.pathname === "/_internal/rpc" && req.method === "POST") {
|
|
84
|
+
const body = (await req.json()) as { target: string; method: string; args: unknown };
|
|
85
|
+
const targetPort = registry.get(body.target);
|
|
86
|
+
if (!targetPort) {
|
|
87
|
+
return Response.json(
|
|
88
|
+
{ error: `plugin "${body.target}" is not loaded in this dev session` },
|
|
89
|
+
{ status: 503 },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const upstream = await fetch(
|
|
94
|
+
`http://127.0.0.1:${targetPort}/_internal/services/${encodeURIComponent(body.method)}`,
|
|
95
|
+
{
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
"content-type": "application/json",
|
|
99
|
+
"x-kserp-internal": DEV_SECRET,
|
|
100
|
+
...(req.headers.get("x-kserp-identity")
|
|
101
|
+
? { "x-kserp-identity": req.headers.get("x-kserp-identity")! }
|
|
102
|
+
: {}),
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify(body.args ?? {}),
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
return new Response(await upstream.text(), {
|
|
108
|
+
status: upstream.status,
|
|
109
|
+
headers: { "content-type": "application/json" },
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
// target loaded but unreachable (still booting / crashed) → 503 = degrade.
|
|
113
|
+
return Response.json({ error: `plugin "${body.target}" unreachable` }, { status: 503 });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// api.assets (A1) in the toolkit: a plugin relays upload/presign/delete here.
|
|
117
|
+
const asset = await tryAssetRelay(url.pathname, req);
|
|
118
|
+
if (asset) return asset;
|
|
119
|
+
if (url.pathname === "/" || url.pathname === "/_dev/status") {
|
|
120
|
+
return Response.json({
|
|
121
|
+
mediator: `http://127.0.0.1:${mediatorPort}`,
|
|
122
|
+
plugins: loaded.map((l) => ({ name: l.name, port: l.port, status: l.status })),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return new Response("dev-kernel", { status: 404 });
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── spawn each plugin (Bun + preload → SQLite + synthetic admin) ──
|
|
130
|
+
for (const l of loaded) {
|
|
131
|
+
const manifest = JSON.parse(
|
|
132
|
+
readFileSync(join(l.dir, "plugin.manifest.json"), "utf8"),
|
|
133
|
+
) as Manifest;
|
|
134
|
+
l.proc = spawn({
|
|
135
|
+
cmd: ["bun", "--preload", PRELOAD, join(l.dir, "server", "main.ts")],
|
|
136
|
+
cwd: l.dir,
|
|
137
|
+
env: {
|
|
138
|
+
...process.env,
|
|
139
|
+
KSERP_DEV_PLUGIN_DIR: l.dir,
|
|
140
|
+
KSERP_DB_ENGINE: "sqlite",
|
|
141
|
+
KSERP_PLUGIN_PORT: String(l.port),
|
|
142
|
+
KSERP_PLUGIN_BIND: "127.0.0.1",
|
|
143
|
+
KSERP_PLUGIN_SCHEMAS: (manifest.schemas ?? []).join(","),
|
|
144
|
+
KSERP_KERNEL_URL: `http://127.0.0.1:${mediatorPort}`,
|
|
145
|
+
KSERP_INTERNAL_SECRET: DEV_SECRET,
|
|
146
|
+
},
|
|
147
|
+
stdout: "inherit",
|
|
148
|
+
stderr: "inherit",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── wait for health, report (a plugin whose own boot throws — e.g. a peer-dependent
|
|
153
|
+
// migration — is reported failed, not fatal to the host) ──
|
|
154
|
+
async function waitHealthy(l: Loaded, timeoutMs = 15000): Promise<void> {
|
|
155
|
+
const deadline = Date.now() + timeoutMs;
|
|
156
|
+
while (Date.now() < deadline) {
|
|
157
|
+
try {
|
|
158
|
+
const r = await fetch(`http://127.0.0.1:${l.port}/_internal/health`);
|
|
159
|
+
if (r.ok) {
|
|
160
|
+
l.status = "up";
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
/* not up yet */
|
|
165
|
+
}
|
|
166
|
+
if (l.proc.exitCode !== null) {
|
|
167
|
+
l.status = "failed";
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await Bun.sleep(300);
|
|
171
|
+
}
|
|
172
|
+
l.status = l.proc.exitCode === null ? "up" : "failed";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await Promise.all(loaded.map((l) => waitHealthy(l)));
|
|
176
|
+
|
|
177
|
+
console.log("\n────────────────────────────────────────────────────────");
|
|
178
|
+
console.log(` dev kernel http://127.0.0.1:${mediatorPort} (cross-plugin RPC mediator)`);
|
|
179
|
+
for (const l of loaded) {
|
|
180
|
+
const mark = l.status === "up" ? "✓" : "✗";
|
|
181
|
+
console.log(` ${mark} ${l.name.padEnd(20)} http://127.0.0.1:${l.port} [${l.status}]`);
|
|
182
|
+
}
|
|
183
|
+
console.log("────────────────────────────────────────────────────────");
|
|
184
|
+
console.log(
|
|
185
|
+
` ${loaded.filter((l) => l.status === "up").length}/${loaded.length} plugins up. Ctrl-C to stop.\n`,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
function shutdown() {
|
|
189
|
+
for (const l of loaded) l.proc.kill();
|
|
190
|
+
mediator.stop(true);
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
process.on("SIGINT", shutdown);
|
|
194
|
+
process.on("SIGTERM", shutdown);
|