kcg-code 0.0.0-stage → 0.2.0
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 +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabel rute API tunnel publik (`https://<username>.<domain>`, domain dari VPS).
|
|
3
|
+
*
|
|
4
|
+
* - `GET /api/tunnel` status (tanpa token/secret)
|
|
5
|
+
* - `GET /api/tunnel/logs?from=<n>` baris log frpc mulai indeks n
|
|
6
|
+
* - `POST /api/tunnel/connect` mulai device flow -> kode + URL
|
|
7
|
+
* - `POST /api/tunnel/connect/cancel`
|
|
8
|
+
* - `POST /api/tunnel/start` | `/stop` | `/rotate-secret` | `/signout`
|
|
9
|
+
* - `PUT /api/tunnel/provider` `{ provider: "kcg" | "lhr" }` — pindah
|
|
10
|
+
* penyedia; yang lama dimatikan lebih dulu (hanya satu tunnel aktif).
|
|
11
|
+
* - `GET /api/tunnel/lhr`, `/lhr/logs`, `POST /lhr/start` | `/lhr/stop` —
|
|
12
|
+
* tunnel localhost.run (tanpa akun, via ssh).
|
|
13
|
+
*
|
|
14
|
+
* Seluruhnya dijaga `guardRoutes` (sesi + Origin). Tanpa app lock, tunnel
|
|
15
|
+
* ditolak (`TUNNEL_LOCK_REQUIRED`).
|
|
16
|
+
*/
|
|
17
|
+
import { errorStatus, json, readJson, serverError } from "./helpers";
|
|
18
|
+
import type { ApiRouteContext } from "./types";
|
|
19
|
+
|
|
20
|
+
export function tunnelRoutes(ctx: ApiRouteContext) {
|
|
21
|
+
const { tunnel, lhr } = ctx;
|
|
22
|
+
const wrap =
|
|
23
|
+
<A extends unknown[]>(fn: (...a: A) => Promise<Response> | Response) =>
|
|
24
|
+
async (...a: A) => {
|
|
25
|
+
try {
|
|
26
|
+
return await fn(...a);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return serverError(e);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"/api/tunnel": {
|
|
34
|
+
GET: wrap(() =>
|
|
35
|
+
json({ tunnel: tunnel.status(), provider: ctx.tunnelProvider(), lhr: lhr.status() }),
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"/api/tunnel/logs": {
|
|
40
|
+
GET: wrap((req: Request) => {
|
|
41
|
+
const from = Number(new URL(req.url).searchParams.get("from") ?? "0");
|
|
42
|
+
return json(tunnel.logs(Number.isFinite(from) && from >= 0 ? from : 0));
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"/api/tunnel/connect": {
|
|
47
|
+
POST: wrap(async () => {
|
|
48
|
+
const res = await tunnel.connect();
|
|
49
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
50
|
+
return json({ tunnel: res.data });
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
"/api/tunnel/connect/cancel": {
|
|
55
|
+
POST: wrap(() => {
|
|
56
|
+
tunnel.cancelConnect();
|
|
57
|
+
return json({ tunnel: tunnel.status() });
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
"/api/tunnel/start": {
|
|
62
|
+
POST: wrap(async () => {
|
|
63
|
+
await lhr.stop();
|
|
64
|
+
ctx.setTunnelProvider("kcg");
|
|
65
|
+
const res = await tunnel.start();
|
|
66
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
67
|
+
return json({ tunnel: res.data });
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
"/api/tunnel/stop": {
|
|
72
|
+
POST: wrap(async () => json({ tunnel: await tunnel.stop() })),
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"/api/tunnel/rotate-secret": {
|
|
76
|
+
POST: wrap(async () => {
|
|
77
|
+
const res = await tunnel.rotateSecret();
|
|
78
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
79
|
+
return json({ tunnel: res.data });
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
"/api/tunnel/signout": {
|
|
84
|
+
POST: wrap(async () => json({ tunnel: await tunnel.signOut() })),
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// ---- Pilihan penyedia (satu tunnel aktif) ----
|
|
88
|
+
"/api/tunnel/provider": {
|
|
89
|
+
PUT: wrap(async (req: Request) => {
|
|
90
|
+
const body = await readJson(req);
|
|
91
|
+
if (!body.ok) return json({ error: "INVALID_JSON" }, 400);
|
|
92
|
+
const provider = (body.data as { provider?: unknown }).provider;
|
|
93
|
+
if (provider !== "kcg" && provider !== "lhr") {
|
|
94
|
+
return json({ error: "INVALID_PROVIDER" }, 400);
|
|
95
|
+
}
|
|
96
|
+
// Matikan penyedia lain dulu: dua alamat publik sekaligus membingungkan
|
|
97
|
+
// dan memperluas permukaan akses.
|
|
98
|
+
if (provider === "lhr") await tunnel.stop();
|
|
99
|
+
else await lhr.stop();
|
|
100
|
+
ctx.setTunnelProvider(provider);
|
|
101
|
+
return json({ provider, tunnel: tunnel.status(), lhr: lhr.status() });
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// ---- localhost.run ----
|
|
106
|
+
"/api/tunnel/lhr": {
|
|
107
|
+
GET: wrap(() => json({ lhr: lhr.status() })),
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
"/api/tunnel/lhr/logs": {
|
|
111
|
+
GET: wrap((req: Request) => {
|
|
112
|
+
const from = Number(new URL(req.url).searchParams.get("from") ?? "0");
|
|
113
|
+
return json(lhr.logs(Number.isFinite(from) && from >= 0 ? from : 0));
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
"/api/tunnel/lhr/start": {
|
|
118
|
+
POST: wrap(async () => {
|
|
119
|
+
// Pastikan hanya satu tunnel publik aktif.
|
|
120
|
+
await tunnel.stop();
|
|
121
|
+
ctx.setTunnelProvider("lhr");
|
|
122
|
+
const res = await lhr.start();
|
|
123
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
124
|
+
return json({ lhr: res.data });
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
"/api/tunnel/lhr/stop": {
|
|
129
|
+
POST: wrap(async () => json({ lhr: await lhr.stop() })),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipe bersama tabel rute API (pola `routes/types.ts` kcgcode).
|
|
3
|
+
*
|
|
4
|
+
* Composition root (`app.ts`) merakit service lalu menyuntikkannya ke tiap
|
|
5
|
+
* tabel rute via `ApiRouteContext`; handler tidak lagi menutup dependency
|
|
6
|
+
* secara langsung seperti saat semuanya menumpuk di `app.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TunnelProvider } from "../db/tunnel";
|
|
10
|
+
import type { AttachmentManager } from "../services/attachments";
|
|
11
|
+
import type { ProjectManager } from "../services/project-manager";
|
|
12
|
+
import type { SessionManager } from "../services/session-manager";
|
|
13
|
+
import type { SkillInstallJobs } from "../services/skill-install-jobs";
|
|
14
|
+
import type { SkillsRegistry } from "../services/skills-registry";
|
|
15
|
+
import type { TunnelManager } from "../services/tunnel";
|
|
16
|
+
import type { LhrManager } from "../services/tunnel-lhr";
|
|
17
|
+
|
|
18
|
+
export interface ApiRouteContext {
|
|
19
|
+
projectManager: ProjectManager;
|
|
20
|
+
sessionManager: SessionManager;
|
|
21
|
+
attachments: AttachmentManager;
|
|
22
|
+
skillsRegistry: SkillsRegistry;
|
|
23
|
+
skillInstalls: SkillInstallJobs;
|
|
24
|
+
tunnel: TunnelManager;
|
|
25
|
+
/** Tunnel localhost.run (alternatif tanpa akun). */
|
|
26
|
+
lhr: LhrManager;
|
|
27
|
+
/** Penyedia remote access terpilih. */
|
|
28
|
+
tunnelProvider: () => TunnelProvider;
|
|
29
|
+
setTunnelProvider: (provider: TunnelProvider) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Beri tahu semua klien bahwa daftar Project/Session berubah (buat/hapus)
|
|
32
|
+
* agar sidebar & daftar di perangkat lain ikut terbarui.
|
|
33
|
+
*/
|
|
34
|
+
notifyDataChanged: () => void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabel rute muat & hapus lampiran gambar (`/api/uploads/:sessionId/:id`).
|
|
3
|
+
*
|
|
4
|
+
* GET mengalirkan bytes gambar untuk bubble & reattach; DELETE menghapus
|
|
5
|
+
* lampiran yang belum terkirim (pengguna membatalkan). Dipasang composition
|
|
6
|
+
* root (`app.ts`) — pola kcgcode (`*.routes.ts`).
|
|
7
|
+
*/
|
|
8
|
+
import type { BunRequest } from "bun";
|
|
9
|
+
import { isPreviewableImage } from "../../lib/attachments";
|
|
10
|
+
import { errorStatus, json, serverError } from "./helpers";
|
|
11
|
+
import type { ApiRouteContext } from "./types";
|
|
12
|
+
|
|
13
|
+
export function uploadsRoutes(ctx: ApiRouteContext) {
|
|
14
|
+
const { attachments } = ctx;
|
|
15
|
+
return {
|
|
16
|
+
// ---- Muat gambar upload (render bubble & reattach) ----
|
|
17
|
+
"/api/uploads/:sessionId/:id": {
|
|
18
|
+
GET: (req: BunRequest<"/api/uploads/:sessionId/:id">) => {
|
|
19
|
+
try {
|
|
20
|
+
const res = attachments.read(req.params.sessionId, req.params.id);
|
|
21
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
22
|
+
// Salin ke Uint8Array ber-buffer ArrayBuffer agar lolos tipe BodyInit.
|
|
23
|
+
const bytes = new Uint8Array(res.data.bytes);
|
|
24
|
+
// Lampiran bisa file apa saja (html/svg/js …) dan disajikan dari
|
|
25
|
+
// origin yang sama dengan aplikasi: hanya gambar raster yang boleh
|
|
26
|
+
// tampil inline; sisanya dipaksa unduh + nosniff + CSP sandbox agar
|
|
27
|
+
// tidak pernah dieksekusi browser (XSS).
|
|
28
|
+
const inline = isPreviewableImage(res.data.mime);
|
|
29
|
+
const name = encodeURIComponent(res.data.filename);
|
|
30
|
+
return new Response(new Blob([bytes]), {
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": inline ? res.data.mime : "application/octet-stream",
|
|
33
|
+
"content-disposition": `${inline ? "inline" : "attachment"}; filename*=UTF-8''${name}`,
|
|
34
|
+
"x-content-type-options": "nosniff",
|
|
35
|
+
"content-security-policy": "default-src 'none'; sandbox",
|
|
36
|
+
"cache-control": "private, max-age=3600",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return serverError(e);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
/** Hapus lampiran yang belum terkirim (pengguna membatalkan). */
|
|
44
|
+
DELETE: (req: BunRequest<"/api/uploads/:sessionId/:id">) => {
|
|
45
|
+
try {
|
|
46
|
+
attachments.remove(req.params.sessionId, req.params.id);
|
|
47
|
+
return json({ ok: true });
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return serverError(e);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabel rute API Workspace (Sandbox_Root) — Settings -> Workspace.
|
|
3
|
+
*
|
|
4
|
+
* - `GET /api/workspace` root aktif + jumlah Project di dalamnya
|
|
5
|
+
* - `PUT /api/workspace` `{ path, create?, current? }` ganti root
|
|
6
|
+
*
|
|
7
|
+
* Keduanya dijaga `guardRoutes` (sesi + Origin). Mengganti root adalah
|
|
8
|
+
* perubahan batas keamanan, jadi saat app dikunci `current` (PIN/password)
|
|
9
|
+
* wajib benar — dengan rate limit yang sama seperti login.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdirSync } from "node:fs";
|
|
12
|
+
import type { Server } from "bun";
|
|
13
|
+
import type { AuthService } from "../services/auth";
|
|
14
|
+
import {
|
|
15
|
+
displayPath,
|
|
16
|
+
projectsOutside,
|
|
17
|
+
validateWorkspaceRoot,
|
|
18
|
+
type WorkspaceInfo,
|
|
19
|
+
writeSandboxRoot,
|
|
20
|
+
} from "../services/workspace";
|
|
21
|
+
import { clientIp } from "./auth-guard";
|
|
22
|
+
import { errorStatus, json, readJson, serverError } from "./helpers";
|
|
23
|
+
import type { ApiRouteContext } from "./types";
|
|
24
|
+
|
|
25
|
+
export interface WorkspaceRouteDeps {
|
|
26
|
+
auth: AuthService;
|
|
27
|
+
configPath: string;
|
|
28
|
+
/** Folder milik KCG Code yang tak boleh tumpang tindih dengan workspace. */
|
|
29
|
+
protectedDirs: string[];
|
|
30
|
+
/** Home pengguna (`~`) — diinjeksi agar test tidak menyentuh home asli. */
|
|
31
|
+
home?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rateLimitedResponse(code: string): Response | null {
|
|
35
|
+
if (!code.startsWith("AUTH_RATE_LIMITED:")) return null;
|
|
36
|
+
const wait = Number(code.slice("AUTH_RATE_LIMITED:".length)) || 30;
|
|
37
|
+
return new Response(JSON.stringify({ error: "AUTH_RATE_LIMITED", retryAfterSec: wait }), {
|
|
38
|
+
status: 429,
|
|
39
|
+
headers: { "content-type": "application/json", "retry-after": String(wait) },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function workspaceRoutes(ctx: ApiRouteContext, deps: WorkspaceRouteDeps) {
|
|
44
|
+
const { projectManager } = ctx;
|
|
45
|
+
|
|
46
|
+
const info = (): WorkspaceInfo => {
|
|
47
|
+
const root = projectManager.getSandboxRoot();
|
|
48
|
+
const all = projectManager.listProjects();
|
|
49
|
+
return {
|
|
50
|
+
root,
|
|
51
|
+
display: displayPath(root, deps.home),
|
|
52
|
+
projectCount: all.length - projectsOutside(root, all).length,
|
|
53
|
+
configPath: deps.configPath,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"/api/workspace": {
|
|
59
|
+
GET: () => {
|
|
60
|
+
try {
|
|
61
|
+
return json({ workspace: info() });
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return serverError(e);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
PUT: async (req: Request, server: Server<unknown>) => {
|
|
67
|
+
try {
|
|
68
|
+
const body = await readJson(req);
|
|
69
|
+
if (!body.ok || body.data === null || typeof body.data !== "object") {
|
|
70
|
+
return json({ error: "INVALID_JSON" }, 400);
|
|
71
|
+
}
|
|
72
|
+
const b = body.data as { path?: unknown; create?: unknown; current?: unknown };
|
|
73
|
+
|
|
74
|
+
// 1. Konfirmasi sandi (bila dikunci) SEBELUM menyentuh filesystem —
|
|
75
|
+
// pesan validasi path tidak boleh jadi oracle tanpa sandi.
|
|
76
|
+
if (deps.auth.isProtected()) {
|
|
77
|
+
const check = await deps.auth.internal.checkCurrentSecret(
|
|
78
|
+
typeof b.current === "string" ? b.current : "",
|
|
79
|
+
clientIp(req, server),
|
|
80
|
+
);
|
|
81
|
+
if (!check.ok) {
|
|
82
|
+
const code = check.error ?? "CURRENT_SECRET_INVALID";
|
|
83
|
+
return rateLimitedResponse(code) ?? json({ error: code }, errorStatus(code));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Validasi path kandidat.
|
|
88
|
+
const v = validateWorkspaceRoot(b.path, {
|
|
89
|
+
create: b.create === true,
|
|
90
|
+
protectedDirs: deps.protectedDirs,
|
|
91
|
+
home: deps.home,
|
|
92
|
+
});
|
|
93
|
+
if (!v.ok) return json({ error: v.error }, errorStatus(v.error));
|
|
94
|
+
const root = v.data.root;
|
|
95
|
+
|
|
96
|
+
// 3. Project terdaftar di luar root baru -> tolak (dengan daftarnya).
|
|
97
|
+
const outside = projectsOutside(root, projectManager.listProjects());
|
|
98
|
+
if (outside.length > 0) {
|
|
99
|
+
return json(
|
|
100
|
+
{
|
|
101
|
+
error: "WORKSPACE_PROJECTS_OUTSIDE",
|
|
102
|
+
projects: outside.map((p) => ({ id: p.id, name: p.name, path: p.path })),
|
|
103
|
+
},
|
|
104
|
+
409,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 4. Buat folder (opsional), simpan ke config, terapkan.
|
|
109
|
+
if (v.data.willCreate) {
|
|
110
|
+
try {
|
|
111
|
+
mkdirSync(root);
|
|
112
|
+
} catch {
|
|
113
|
+
return json({ error: "WORKSPACE_CREATE_FAILED" }, 500);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const saved = writeSandboxRoot(deps.configPath, root);
|
|
117
|
+
if (!saved.ok) return json({ error: saved.error }, 500);
|
|
118
|
+
projectManager.setSandboxRoot(root);
|
|
119
|
+
ctx.notifyDataChanged();
|
|
120
|
+
return json({ workspace: info() });
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return serverError(e);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveVersionLabel } from "../app-info";
|
|
3
|
+
|
|
4
|
+
describe("resolveVersionLabel", () => {
|
|
5
|
+
test("bun dev (mode dev, tanpa NODE_ENV) -> 'dev'", () => {
|
|
6
|
+
expect(resolveVersionLabel("0.1.0", "dev", undefined)).toBe("dev");
|
|
7
|
+
expect(resolveVersionLabel("0.1.0", "dev", "development")).toBe("dev");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("bun start (mode dev + NODE_ENV=production) -> versi package", () => {
|
|
11
|
+
expect(resolveVersionLabel("0.1.0", "dev", "production")).toBe("0.1.0");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("CLI global -> selalu versi package", () => {
|
|
15
|
+
expect(resolveVersionLabel("1.2.3", "cli", undefined)).toBe("1.2.3");
|
|
16
|
+
expect(resolveVersionLabel("1.2.3", "cli", "production")).toBe("1.2.3");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test `attachments.ts` — Attachment_Store gambar upload.
|
|
3
|
+
*
|
|
4
|
+
* Yang diuji (tanpa server sungguhan):
|
|
5
|
+
* - save menerima semua jenis file + batas ukuran; mime & nama dinormalkan.
|
|
6
|
+
* - id dibangkitkan server (UUID) dan path aman dari traversal.
|
|
7
|
+
* - info/read mengembalikan metadata & bytes yang sama (round-trip).
|
|
8
|
+
* - remove & removeSession membersihkan file.
|
|
9
|
+
*/
|
|
10
|
+
import { expect, test } from "bun:test";
|
|
11
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { createAttachmentManager, MAX_UPLOAD_BYTES } from "../attachments";
|
|
15
|
+
|
|
16
|
+
function freshRoot(): string {
|
|
17
|
+
return mkdtempSync(path.join(tmpdir(), "kcg-attach-"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tinyPng(): Uint8Array {
|
|
21
|
+
return new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("save + info + read: round-trip bytes & metadata per Session", () => {
|
|
25
|
+
const root = freshRoot();
|
|
26
|
+
try {
|
|
27
|
+
const m = createAttachmentManager(root);
|
|
28
|
+
const saved = m.save("ses1", "foto.png", "image/png", tinyPng());
|
|
29
|
+
expect(saved.ok).toBe(true);
|
|
30
|
+
if (!saved.ok) return;
|
|
31
|
+
expect(saved.data.id).toMatch(/^[0-9a-f-]{36}$/);
|
|
32
|
+
|
|
33
|
+
const info = m.info("ses1", saved.data.id);
|
|
34
|
+
expect(info.ok).toBe(true);
|
|
35
|
+
if (info.ok) {
|
|
36
|
+
expect(info.data.filename).toBe("foto.png");
|
|
37
|
+
expect(info.data.mime).toBe("image/png");
|
|
38
|
+
expect(info.data.absPath).toContain(saved.data.id);
|
|
39
|
+
expect(existsSync(info.data.absPath)).toBe(true);
|
|
40
|
+
}
|
|
41
|
+
const read = m.read("ses1", saved.data.id);
|
|
42
|
+
expect(read.ok).toBe(true);
|
|
43
|
+
if (read.ok) {
|
|
44
|
+
expect([...read.data.bytes]).toEqual([...tinyPng()]);
|
|
45
|
+
expect(read.data.mime).toBe("image/png");
|
|
46
|
+
}
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(root, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("save menerima file apa saja, menolak file kosong dan file > 20 MiB", () => {
|
|
53
|
+
const root = freshRoot();
|
|
54
|
+
try {
|
|
55
|
+
const m = createAttachmentManager(root);
|
|
56
|
+
const txt = m.save("s", "a.txt", "text/plain", new TextEncoder().encode("x"));
|
|
57
|
+
expect(txt.ok).toBe(true);
|
|
58
|
+
const zip = m.save("s", "arsip.zip", "application/zip", new Uint8Array([1, 2]));
|
|
59
|
+
expect(zip.ok && zip.data.mime).toBe("application/zip");
|
|
60
|
+
expect(m.save("s", "a.png", "image/png", new Uint8Array(0)).ok).toBe(false);
|
|
61
|
+
const big = m.save("s", "a.png", "image/png", new Uint8Array(MAX_UPLOAD_BYTES + 1));
|
|
62
|
+
expect(!big.ok && big.error).toBe("FILE_TOO_LARGE");
|
|
63
|
+
} finally {
|
|
64
|
+
rmSync(root, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("save menormalkan mime & nama file (tanpa path / karakter kontrol)", () => {
|
|
69
|
+
const root = freshRoot();
|
|
70
|
+
try {
|
|
71
|
+
const m = createAttachmentManager(root);
|
|
72
|
+
const r = m.save("s", "../../etc/pa\nss.txt", "text/html\r\nx: y", new Uint8Array([1]));
|
|
73
|
+
expect(r.ok).toBe(true);
|
|
74
|
+
if (r.ok) {
|
|
75
|
+
expect(r.data.filename).toBe("pass.txt");
|
|
76
|
+
expect(r.data.mime).toBe("application/octet-stream");
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("id tak dikenal / bukan UUID -> info & read gagal tanpa akses path", () => {
|
|
84
|
+
const root = freshRoot();
|
|
85
|
+
try {
|
|
86
|
+
const m = createAttachmentManager(root);
|
|
87
|
+
expect(m.info("ses1", "00000000-0000-0000-0000-000000000000").ok).toBe(false);
|
|
88
|
+
expect(m.read("ses1", "../../etc/passwd").ok).toBe(false); // traversal ditolak
|
|
89
|
+
expect(m.info("ses1", "bukan-uuid").ok).toBe(false);
|
|
90
|
+
} finally {
|
|
91
|
+
rmSync(root, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("info.absPath selalu absolut walau uploadsRoot relatif (agar URL file:// valid untuk opencode)", () => {
|
|
96
|
+
const dir = freshRoot();
|
|
97
|
+
try {
|
|
98
|
+
// uploadsRoot relatif (mis. `data/uploads`) — persis konfigurasi default
|
|
99
|
+
// app yang dulu menghasilkan URL `file://data/…` (host="data") sehingga
|
|
100
|
+
// opencode menolak prompt ("File URL host must be localhost or empty").
|
|
101
|
+
const relRoot = path.relative(process.cwd(), dir);
|
|
102
|
+
expect(path.isAbsolute(relRoot)).toBe(false);
|
|
103
|
+
const m = createAttachmentManager(relRoot);
|
|
104
|
+
const saved = m.save("ses1", "a.png", "image/png", tinyPng());
|
|
105
|
+
expect(saved.ok).toBe(true);
|
|
106
|
+
if (!saved.ok) return;
|
|
107
|
+
const info = m.info("ses1", saved.data.id);
|
|
108
|
+
expect(info.ok).toBe(true);
|
|
109
|
+
if (info.ok) {
|
|
110
|
+
expect(path.isAbsolute(info.data.absPath)).toBe(true);
|
|
111
|
+
expect(info.data.absPath.startsWith("/")).toBe(true);
|
|
112
|
+
expect(existsSync(info.data.absPath)).toBe(true);
|
|
113
|
+
// URL file:// yang dibangun dari absPath harus hostless (`file:///…`).
|
|
114
|
+
const url = `file://${info.data.absPath}`;
|
|
115
|
+
expect(url.startsWith("file:///")).toBe(true);
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("remove menghapus file & metadata; removeSession menghapus seluruh Session", () => {
|
|
123
|
+
const root = freshRoot();
|
|
124
|
+
try {
|
|
125
|
+
const m = createAttachmentManager(root);
|
|
126
|
+
const s1 = m.save("s1", "a.png", "image/png", tinyPng());
|
|
127
|
+
const s2 = m.save("s2", "b.jpg", "image/jpeg", new Uint8Array([9, 9]));
|
|
128
|
+
expect(s1.ok && s2.ok).toBe(true);
|
|
129
|
+
if (!s1.ok || !s2.ok) return;
|
|
130
|
+
|
|
131
|
+
m.remove("s1", s1.data.id);
|
|
132
|
+
expect(m.info("s1", s1.data.id).ok).toBe(false);
|
|
133
|
+
expect(m.info("s2", s2.data.id).ok).toBe(true); // Session lain tidak tersentuh
|
|
134
|
+
|
|
135
|
+
m.removeSession("s2");
|
|
136
|
+
expect(m.info("s2", s2.data.id).ok).toBe(false);
|
|
137
|
+
expect(readdirSync(path.join(root, "s1"))).toHaveLength(0);
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(root, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("Session berbeda terisolasi walau nama file sama", () => {
|
|
144
|
+
const root = freshRoot();
|
|
145
|
+
try {
|
|
146
|
+
const m = createAttachmentManager(root);
|
|
147
|
+
const a = m.save("s1", "x.png", "image/png", new Uint8Array([1]));
|
|
148
|
+
const b = m.save("s2", "x.png", "image/png", new Uint8Array([2]));
|
|
149
|
+
expect(a.ok && b.ok).toBe(true);
|
|
150
|
+
if (!a.ok || !b.ok) return;
|
|
151
|
+
expect(a.data.id).not.toBe(b.data.id);
|
|
152
|
+
const ra = m.read("s1", a.data.id);
|
|
153
|
+
const rb = m.read("s2", b.data.id);
|
|
154
|
+
expect(ra.ok && rb.ok).toBe(true);
|
|
155
|
+
if (ra.ok && rb.ok) expect([...ra.data.bytes]).toEqual([1]);
|
|
156
|
+
} finally {
|
|
157
|
+
rmSync(root, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|