@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AssistantContentBlock, ToolResultMessage } from "./types";
|
|
2
|
+
import { resolveLocalFilePath } from "./file-links";
|
|
3
|
+
import { isEditToolName, isWriteToolName } from "./tool-names";
|
|
4
|
+
|
|
5
|
+
export interface WrittenFile {
|
|
6
|
+
/** Resolved absolute path of a file this turn wrote. */
|
|
7
|
+
filePath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isFileWritingToolName(toolName: string): boolean {
|
|
11
|
+
return isWriteToolName(toolName) || isEditToolName(toolName);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readToolPath(input: Record<string, unknown> | undefined): string | null {
|
|
15
|
+
if (!input) return null;
|
|
16
|
+
const value = input.file_path ?? input.path;
|
|
17
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Collect the distinct files a single assistant turn actually wrote.
|
|
22
|
+
*
|
|
23
|
+
* Every entry is derived from a `write`/`edit` tool call whose result arrived
|
|
24
|
+
* and did not error — never from the reply text. A path the assistant merely
|
|
25
|
+
* mentions in prose is not evidence that any file was touched, so it is not a
|
|
26
|
+
* source here; the tool call is the record of what happened.
|
|
27
|
+
*
|
|
28
|
+
* Paths are resolved against `cwd`, deduped, and kept in first-seen order.
|
|
29
|
+
*/
|
|
30
|
+
export function extractTurnWrittenFiles(
|
|
31
|
+
content: AssistantContentBlock[],
|
|
32
|
+
toolResults: Map<string, ToolResultMessage> | undefined,
|
|
33
|
+
cwd?: string,
|
|
34
|
+
): WrittenFile[] {
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
const writtenFiles: WrittenFile[] = [];
|
|
37
|
+
|
|
38
|
+
for (const block of content) {
|
|
39
|
+
if (block.type !== "toolCall") continue;
|
|
40
|
+
if (!isFileWritingToolName(block.toolName)) continue;
|
|
41
|
+
|
|
42
|
+
// No result yet (still streaming) or the call failed — nothing was written.
|
|
43
|
+
const result = toolResults?.get(block.toolCallId);
|
|
44
|
+
if (!result || result.isError) continue;
|
|
45
|
+
|
|
46
|
+
const rawPath = readToolPath(block.input);
|
|
47
|
+
if (!rawPath) continue;
|
|
48
|
+
|
|
49
|
+
// Tool arguments are filesystem paths, not hrefs: preserve characters such
|
|
50
|
+
// as #, ?, and :digits that have special meaning in links and source refs.
|
|
51
|
+
const filePath = resolveLocalFilePath(rawPath, cwd);
|
|
52
|
+
if (!filePath) continue;
|
|
53
|
+
|
|
54
|
+
if (seen.has(filePath)) continue;
|
|
55
|
+
seen.add(filePath);
|
|
56
|
+
writtenFiles.push({ filePath });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return writtenFiles;
|
|
60
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Types mirrored from pi-mono coding-agent session-manager
|
|
2
|
+
|
|
3
|
+
export interface SessionHeader {
|
|
4
|
+
type: "session";
|
|
5
|
+
version?: number;
|
|
6
|
+
id: string;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
parentSession?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SessionEntryBase {
|
|
13
|
+
type: string;
|
|
14
|
+
id: string;
|
|
15
|
+
parentId: string | null;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TextContent {
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ImageContent {
|
|
25
|
+
type: "image";
|
|
26
|
+
source: {
|
|
27
|
+
type: "base64" | "url";
|
|
28
|
+
media_type?: string;
|
|
29
|
+
data?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ThinkingContent {
|
|
35
|
+
type: "thinking";
|
|
36
|
+
thinking: string;
|
|
37
|
+
/** Historical content omitted from the initial response and loaded on demand. */
|
|
38
|
+
deferred?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ToolCallContent {
|
|
42
|
+
type: "toolCall";
|
|
43
|
+
toolCallId: string;
|
|
44
|
+
toolName: string;
|
|
45
|
+
input: Record<string, unknown>;
|
|
46
|
+
/** Client-only buffer for streamed tool input. Never persisted to session files. */
|
|
47
|
+
rawInput?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type AssistantContentBlock = TextContent | ImageContent | ThinkingContent | ToolCallContent;
|
|
51
|
+
|
|
52
|
+
export interface UserMessage {
|
|
53
|
+
role: "user";
|
|
54
|
+
content: string | (TextContent | ImageContent)[];
|
|
55
|
+
timestamp?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AssistantMessage {
|
|
59
|
+
role: "assistant";
|
|
60
|
+
content: AssistantContentBlock[];
|
|
61
|
+
model: string;
|
|
62
|
+
provider: string;
|
|
63
|
+
stopReason?: string;
|
|
64
|
+
errorMessage?: string;
|
|
65
|
+
timestamp?: number;
|
|
66
|
+
usage?: {
|
|
67
|
+
input: number;
|
|
68
|
+
output: number;
|
|
69
|
+
cacheRead: number;
|
|
70
|
+
cacheWrite: number;
|
|
71
|
+
cost: {
|
|
72
|
+
input: number;
|
|
73
|
+
output: number;
|
|
74
|
+
cacheRead: number;
|
|
75
|
+
cacheWrite: number;
|
|
76
|
+
total: number;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ToolResultMessage {
|
|
82
|
+
role: "toolResult";
|
|
83
|
+
toolCallId: string;
|
|
84
|
+
toolName?: string;
|
|
85
|
+
content: (TextContent | ImageContent)[];
|
|
86
|
+
isError?: boolean;
|
|
87
|
+
details?: unknown;
|
|
88
|
+
timestamp?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CustomMessage {
|
|
92
|
+
role: "custom";
|
|
93
|
+
customType: string;
|
|
94
|
+
content: string | (TextContent | ImageContent)[];
|
|
95
|
+
display: boolean;
|
|
96
|
+
details?: unknown;
|
|
97
|
+
timestamp?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BashExecutionMessage {
|
|
101
|
+
role: "bashExecution";
|
|
102
|
+
command: string;
|
|
103
|
+
output: string;
|
|
104
|
+
exitCode?: number;
|
|
105
|
+
cancelled?: boolean;
|
|
106
|
+
truncated?: boolean;
|
|
107
|
+
fullOutputPath?: string;
|
|
108
|
+
excludeFromContext?: boolean;
|
|
109
|
+
timestamp?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type AgentMessage = UserMessage | AssistantMessage | ToolResultMessage | CustomMessage | BashExecutionMessage;
|
|
113
|
+
|
|
114
|
+
export type ExtensionUiRequest =
|
|
115
|
+
| {
|
|
116
|
+
type: "extension_ui_request";
|
|
117
|
+
id: string;
|
|
118
|
+
method: "select";
|
|
119
|
+
title: string;
|
|
120
|
+
options: string[];
|
|
121
|
+
timeout?: number;
|
|
122
|
+
expiresAt?: number;
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
type: "extension_ui_request";
|
|
126
|
+
id: string;
|
|
127
|
+
method: "confirm";
|
|
128
|
+
title: string;
|
|
129
|
+
message: string;
|
|
130
|
+
timeout?: number;
|
|
131
|
+
expiresAt?: number;
|
|
132
|
+
}
|
|
133
|
+
| {
|
|
134
|
+
type: "extension_ui_request";
|
|
135
|
+
id: string;
|
|
136
|
+
method: "input";
|
|
137
|
+
title: string;
|
|
138
|
+
placeholder?: string;
|
|
139
|
+
timeout?: number;
|
|
140
|
+
expiresAt?: number;
|
|
141
|
+
}
|
|
142
|
+
| {
|
|
143
|
+
type: "extension_ui_request";
|
|
144
|
+
id: string;
|
|
145
|
+
method: "editor";
|
|
146
|
+
title: string;
|
|
147
|
+
prefill?: string;
|
|
148
|
+
timeout?: number;
|
|
149
|
+
expiresAt?: number;
|
|
150
|
+
}
|
|
151
|
+
| {
|
|
152
|
+
type: "extension_ui_request";
|
|
153
|
+
id: string;
|
|
154
|
+
method: "notify";
|
|
155
|
+
message: string;
|
|
156
|
+
notifyType?: "info" | "warning" | "error";
|
|
157
|
+
}
|
|
158
|
+
| {
|
|
159
|
+
type: "extension_ui_request";
|
|
160
|
+
id: string;
|
|
161
|
+
method: "setStatus";
|
|
162
|
+
statusKey: string;
|
|
163
|
+
statusText?: string;
|
|
164
|
+
}
|
|
165
|
+
| {
|
|
166
|
+
type: "extension_ui_request";
|
|
167
|
+
id: string;
|
|
168
|
+
method: "setWidget";
|
|
169
|
+
widgetKey: string;
|
|
170
|
+
widgetLines?: string[];
|
|
171
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
172
|
+
}
|
|
173
|
+
| {
|
|
174
|
+
type: "extension_ui_request";
|
|
175
|
+
id: string;
|
|
176
|
+
method: "setTitle";
|
|
177
|
+
title: string;
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
type: "extension_ui_request";
|
|
181
|
+
id: string;
|
|
182
|
+
method: "set_editor_text";
|
|
183
|
+
text: string;
|
|
184
|
+
}
|
|
185
|
+
| {
|
|
186
|
+
type: "extension_ui_request";
|
|
187
|
+
id: string;
|
|
188
|
+
method: "custom";
|
|
189
|
+
lines: string[];
|
|
190
|
+
closed?: boolean;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export type BlockingExtensionUiRequest = Extract<
|
|
194
|
+
ExtensionUiRequest,
|
|
195
|
+
{ method: "select" | "confirm" | "input" | "editor" | "custom" }
|
|
196
|
+
>;
|
|
197
|
+
|
|
198
|
+
export type ExtensionUiResponse =
|
|
199
|
+
| { type: "extension_ui_response"; id: string; value: string }
|
|
200
|
+
| { type: "extension_ui_response"; id: string; confirmed: boolean }
|
|
201
|
+
| { type: "extension_ui_response"; id: string; cancelled: true };
|
|
202
|
+
|
|
203
|
+
export interface ExtensionStatusItem {
|
|
204
|
+
key: string;
|
|
205
|
+
text: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ExtensionWidgetItem {
|
|
209
|
+
key: string;
|
|
210
|
+
lines: string[];
|
|
211
|
+
placement: "aboveEditor" | "belowEditor";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface SessionMessageEntry extends SessionEntryBase {
|
|
215
|
+
type: "message";
|
|
216
|
+
message: AgentMessage;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface ThinkingLevelChangeEntry extends SessionEntryBase {
|
|
220
|
+
type: "thinking_level_change";
|
|
221
|
+
thinkingLevel: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface ModelChangeEntry extends SessionEntryBase {
|
|
225
|
+
type: "model_change";
|
|
226
|
+
provider: string;
|
|
227
|
+
modelId: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface CompactionEntry extends SessionEntryBase {
|
|
231
|
+
type: "compaction";
|
|
232
|
+
summary: string;
|
|
233
|
+
firstKeptEntryId: string;
|
|
234
|
+
tokensBefore: number;
|
|
235
|
+
details?: unknown;
|
|
236
|
+
fromHook?: boolean;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface BranchSummaryEntry extends SessionEntryBase {
|
|
240
|
+
type: "branch_summary";
|
|
241
|
+
fromId: string;
|
|
242
|
+
summary: string;
|
|
243
|
+
details?: unknown;
|
|
244
|
+
fromHook?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface CustomEntry extends SessionEntryBase {
|
|
248
|
+
type: "custom";
|
|
249
|
+
customType: string;
|
|
250
|
+
data?: unknown;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface CustomMessageEntry extends SessionEntryBase {
|
|
254
|
+
type: "custom_message";
|
|
255
|
+
customType: string;
|
|
256
|
+
content: string | (TextContent | ImageContent)[];
|
|
257
|
+
details?: unknown;
|
|
258
|
+
display: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface LabelEntry extends SessionEntryBase {
|
|
262
|
+
type: "label";
|
|
263
|
+
targetId: string;
|
|
264
|
+
label: string | undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface SessionInfoEntry extends SessionEntryBase {
|
|
268
|
+
type: "session_info";
|
|
269
|
+
name?: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export type SessionEntry =
|
|
273
|
+
| SessionMessageEntry
|
|
274
|
+
| ThinkingLevelChangeEntry
|
|
275
|
+
| ModelChangeEntry
|
|
276
|
+
| CompactionEntry
|
|
277
|
+
| BranchSummaryEntry
|
|
278
|
+
| CustomEntry
|
|
279
|
+
| CustomMessageEntry
|
|
280
|
+
| LabelEntry
|
|
281
|
+
| SessionInfoEntry;
|
|
282
|
+
|
|
283
|
+
export type FileEntry = SessionHeader | SessionEntry;
|
|
284
|
+
|
|
285
|
+
export interface BranchPreview {
|
|
286
|
+
role?: "user" | "assistant";
|
|
287
|
+
text: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface SessionTreeNode {
|
|
291
|
+
entry: SessionEntry;
|
|
292
|
+
children: SessionTreeNode[];
|
|
293
|
+
label?: string;
|
|
294
|
+
compressedEntryIds?: string[];
|
|
295
|
+
branchPreview?: BranchPreview;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface SessionInfo {
|
|
299
|
+
path: string;
|
|
300
|
+
id: string;
|
|
301
|
+
cwd: string;
|
|
302
|
+
name?: string;
|
|
303
|
+
created: string;
|
|
304
|
+
modified: string;
|
|
305
|
+
messageCount: number;
|
|
306
|
+
firstMessage: string;
|
|
307
|
+
/** 最后一条 assistant 回复文本(看板卡片中间区展示用,截断),无则为空串 */
|
|
308
|
+
lastReply?: string;
|
|
309
|
+
parentSessionId?: string; // set if this session was forked from another
|
|
310
|
+
/** Main repo root shared by all worktrees of this cwd (cwd itself for non-git dirs).
|
|
311
|
+
* Always set by the server; optional because the client builds transient
|
|
312
|
+
* SessionInfo objects before the first refresh. Fall back to cwd. */
|
|
313
|
+
projectRoot?: string;
|
|
314
|
+
/** Stable server-computed project identity for grouping and comparison.
|
|
315
|
+
* Unlike projectRoot, Windows keys are case- and separator-insensitive.
|
|
316
|
+
* Internal only: use projectRoot/cwd for display and filesystem operations. */
|
|
317
|
+
projectKey?: string;
|
|
318
|
+
/** Branch name when cwd is a linked git worktree (not the main checkout) */
|
|
319
|
+
worktreeBranch?: string;
|
|
320
|
+
/** Git branch of the session's project root (any git repo, not only worktrees). */
|
|
321
|
+
branch?: string;
|
|
322
|
+
/** True when cwd is a linked git worktree (not the main checkout). */
|
|
323
|
+
isWorktree?: boolean;
|
|
324
|
+
/** True while the runtime session exists only in memory and its JSONL file
|
|
325
|
+
* has not been created yet. Disk-backed actions must wait until this clears. */
|
|
326
|
+
transient?: boolean;
|
|
327
|
+
/** Pinned by the user; sorts first inside its own region (task group or chat). */
|
|
328
|
+
pinned?: boolean;
|
|
329
|
+
/** Live task ownership (sessionInfo panel reads it realtime to avoid stale snapshots). */
|
|
330
|
+
taskName?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface TodoItem {
|
|
334
|
+
id?: string;
|
|
335
|
+
content: string;
|
|
336
|
+
status?: string;
|
|
337
|
+
priority?: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface SessionContext {
|
|
341
|
+
messages: AgentMessage[];
|
|
342
|
+
entryIds: string[]; // parallel to messages — the session entry id for each message
|
|
343
|
+
/** Parallel to entryIds — the parent entry id of each message's session entry (null = root). */
|
|
344
|
+
parentIds: (string | null)[];
|
|
345
|
+
thinkingLevel: string;
|
|
346
|
+
model: { provider: string; modelId: string } | null;
|
|
347
|
+
// Latest todo list published by pi (pi-todo.state custom entries), empty when none.
|
|
348
|
+
todos: TodoItem[];
|
|
349
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wallpaper display settings:
|
|
7
|
+
* - repeat : tile horizontally (`background-repeat: repeat-x`)
|
|
8
|
+
* - offsetX : horizontal drag offset in px, applied as `calc(50% + Npx)`
|
|
9
|
+
* - fill : fill leftover side gaps with colours sampled from the
|
|
10
|
+
* wallpaper's left/right edges (only meaningful when not tiling)
|
|
11
|
+
* - bubbleOpacity : chat-bubble glass opacity in percent (0–100), written to
|
|
12
|
+
* `--bubble-alpha` on <html>
|
|
13
|
+
* - bubbleBlur : frosted blur radius in px (0–30), written to
|
|
14
|
+
* `--glass-blur-bubble` on <html>
|
|
15
|
+
* - scrimAlpha : board-canvas scrim opacity in percent (0–100), written to
|
|
16
|
+
* `--board-scrim-alpha` on <html> (independent of bubble alpha)
|
|
17
|
+
* - scrimBlur : board-canvas scrim blur radius in px (0–30), written to
|
|
18
|
+
* `--board-scrim-blur` on <html>
|
|
19
|
+
*
|
|
20
|
+
* 看板 scrim:透明度滑块暂注释(微调即对背景影响过大),透明度固定保持 0,
|
|
21
|
+
* 只向用户开放磨砂滑块;scrimAlpha 字段保留以备后续恢复。
|
|
22
|
+
*
|
|
23
|
+
* Settings persist in localStorage and are applied to <html> as CSS custom
|
|
24
|
+
* properties consumed by the wallpaper layer on `body` in app/globals.css.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export type WallpaperSettings = {
|
|
28
|
+
repeat: boolean;
|
|
29
|
+
offsetX: number;
|
|
30
|
+
fill: boolean;
|
|
31
|
+
fillColorLeft: string;
|
|
32
|
+
fillColorRight: string;
|
|
33
|
+
bubbleOpacity: number;
|
|
34
|
+
bubbleBlur: number;
|
|
35
|
+
scrimAlpha: number;
|
|
36
|
+
scrimBlur: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const DEFAULTS: WallpaperSettings = {
|
|
40
|
+
repeat: false,
|
|
41
|
+
offsetX: 0,
|
|
42
|
+
fill: false,
|
|
43
|
+
fillColorLeft: "#000000",
|
|
44
|
+
fillColorRight: "#000000",
|
|
45
|
+
bubbleOpacity: 44,
|
|
46
|
+
bubbleBlur: 8,
|
|
47
|
+
scrimAlpha: 0,
|
|
48
|
+
// 默认无磨砂(0)。打开画布时先按默认渲染,再从存储恢复——
|
|
49
|
+
// 若默认非 0,首帧会先闪一下 2px 再跳变成存储值(见 useWallpaperSettings 惰性初始化注释)
|
|
50
|
+
scrimBlur: 0,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const LS_KEY = "wallpaper-settings";
|
|
54
|
+
|
|
55
|
+
export function loadWallpaperSettings(): WallpaperSettings {
|
|
56
|
+
try {
|
|
57
|
+
const raw = localStorage.getItem(LS_KEY);
|
|
58
|
+
if (!raw) return { ...DEFAULTS };
|
|
59
|
+
return { ...DEFAULTS, ...(JSON.parse(raw) as Partial<WallpaperSettings>) };
|
|
60
|
+
} catch {
|
|
61
|
+
return { ...DEFAULTS };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function saveWallpaperSettings(s: WallpaperSettings): void {
|
|
66
|
+
try {
|
|
67
|
+
localStorage.setItem(LS_KEY, JSON.stringify(s));
|
|
68
|
+
} catch {
|
|
69
|
+
// localStorage unavailable — settings stay session-only.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Push the current settings into CSS custom properties on <html>. */
|
|
74
|
+
export function applyWallpaperCss(s: WallpaperSettings, hasImage: boolean): void {
|
|
75
|
+
const el = document.documentElement.style;
|
|
76
|
+
el.setProperty("--app-bg-repeat", s.repeat ? "repeat-x" : "no-repeat");
|
|
77
|
+
// The wallpaper always keeps its fitted `cover` size. Repeat/fill merely
|
|
78
|
+
// cover the side gaps that open up after a horizontal drag offset — they
|
|
79
|
+
// must never rescale the image itself.
|
|
80
|
+
el.setProperty("--app-bg-size", "cover");
|
|
81
|
+
el.setProperty("--app-bg-pos-x", `calc(50% + ${Math.round(s.offsetX)}px)`);
|
|
82
|
+
el.setProperty("--bubble-alpha", String(s.bubbleOpacity / 100));
|
|
83
|
+
el.setProperty("--glass-blur-bubble", `${Math.round(s.bubbleBlur)}px`);
|
|
84
|
+
// scrim 透明度固定保持 0(透明度滑块暂注释,微调即对背景影响过大),
|
|
85
|
+
// 只写磨砂强度。
|
|
86
|
+
// scrim 磨砂只用 blur、不动饱和度——磨砂本质就是模糊,去饱和/增饱和
|
|
87
|
+
// 都会让壁纸颜色失真,且会受 --glass-saturate(浅 80% / 深 140%)的
|
|
88
|
+
// 主题差异影响。只 blur 则深浅色效果完全一致。
|
|
89
|
+
// 关键:磨砂强度为 0 时把整个 backdrop-filter 置为 none,背景原样。
|
|
90
|
+
el.setProperty("--board-scrim-alpha", String(s.scrimAlpha / 100));
|
|
91
|
+
el.setProperty("--board-scrim-blur", `${Math.round(s.scrimBlur)}px`);
|
|
92
|
+
el.setProperty(
|
|
93
|
+
"--board-scrim-filter",
|
|
94
|
+
s.scrimBlur > 0 ? `blur(${Math.round(s.scrimBlur)}px)` : "none",
|
|
95
|
+
);
|
|
96
|
+
if (s.fill && !s.repeat && hasImage) {
|
|
97
|
+
el.setProperty(
|
|
98
|
+
"--app-bg-fill",
|
|
99
|
+
`linear-gradient(90deg, ${s.fillColorLeft}, ${s.fillColorLeft} 50%, ${s.fillColorRight} 50%, ${s.fillColorRight})`,
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
el.removeProperty("--app-bg-fill");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sample the leftmost/rightmost pixel column of an image blob into two CSS colours.
|
|
108
|
+
* Only samples the top 8 rows — a short vertical slice keeps colours true to the
|
|
109
|
+
* wallpaper edge and avoids the "washed out" result that happens when averaging
|
|
110
|
+
* highlights, shadows and midtones over a taller strip.
|
|
111
|
+
*/
|
|
112
|
+
export async function sampleEdgeColors(
|
|
113
|
+
blob: Blob,
|
|
114
|
+
): Promise<{ left: string; right: string } | null> {
|
|
115
|
+
try {
|
|
116
|
+
const bmp = await createImageBitmap(blob);
|
|
117
|
+
const sampleH = 8;
|
|
118
|
+
const w = Math.max(1, Math.round((bmp.width * sampleH) / bmp.height));
|
|
119
|
+
const canvas = document.createElement("canvas");
|
|
120
|
+
canvas.width = w;
|
|
121
|
+
canvas.height = sampleH;
|
|
122
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
123
|
+
if (!ctx) return null;
|
|
124
|
+
ctx.drawImage(bmp, 0, 0, w, sampleH);
|
|
125
|
+
const avg = (x: number): string => {
|
|
126
|
+
const d = ctx.getImageData(x, 0, 1, sampleH).data;
|
|
127
|
+
let r = 0, g = 0, b = 0;
|
|
128
|
+
for (let i = 0; i < sampleH; i++) {
|
|
129
|
+
r += d[i * 4];
|
|
130
|
+
g += d[i * 4 + 1];
|
|
131
|
+
b += d[i * 4 + 2];
|
|
132
|
+
}
|
|
133
|
+
return `rgb(${Math.round(r / sampleH)}, ${Math.round(g / sampleH)}, ${Math.round(b / sampleH)})`;
|
|
134
|
+
};
|
|
135
|
+
const result = { left: avg(0), right: avg(w - 1) };
|
|
136
|
+
bmp.close();
|
|
137
|
+
return result;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type WallpaperDims = { width: number; height: number };
|
|
144
|
+
|
|
145
|
+
/** Natural dimensions of an image or video URL (video reads metadata only). */
|
|
146
|
+
export function loadMediaDims(
|
|
147
|
+
url: string,
|
|
148
|
+
isVideo: boolean,
|
|
149
|
+
): Promise<WallpaperDims | null> {
|
|
150
|
+
if (isVideo) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const v = document.createElement("video");
|
|
153
|
+
v.preload = "metadata";
|
|
154
|
+
v.onloadedmetadata = () =>
|
|
155
|
+
resolve({ width: v.videoWidth, height: v.videoHeight });
|
|
156
|
+
v.onerror = () => resolve(null);
|
|
157
|
+
v.src = url;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const img = new Image();
|
|
162
|
+
img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
163
|
+
img.onerror = () => resolve(null);
|
|
164
|
+
img.src = url;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Load, persist and live-apply wallpaper display settings. */
|
|
169
|
+
export function useWallpaperSettings(hasImage: boolean) {
|
|
170
|
+
// 惰性初始化直接读 localStorage:首帧即存储值(无存储时为默认)。
|
|
171
|
+
// 不要用 DEFAULTS 起步再挂载后 setSettings 恢复——那会让首帧先应用默认值
|
|
172
|
+
// (如 scrimBlur 2px)再跳变到存储值,打开画布时磨砂强度闪烁一下。
|
|
173
|
+
// SSR 阶段 localStorage 不存在,loadWallpaperSettings 内部 try/catch 返回默认,安全。
|
|
174
|
+
const [settings, setSettings] = useState<WallpaperSettings>(() => loadWallpaperSettings());
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
applyWallpaperCss(settings, hasImage);
|
|
178
|
+
}, [settings, hasImage]);
|
|
179
|
+
|
|
180
|
+
const update = useCallback(
|
|
181
|
+
(patch: Partial<WallpaperSettings>) =>
|
|
182
|
+
setSettings((prev) => {
|
|
183
|
+
const next = { ...prev, ...patch };
|
|
184
|
+
saveWallpaperSettings(next);
|
|
185
|
+
return next;
|
|
186
|
+
}),
|
|
187
|
+
[],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return { settings, update };
|
|
191
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
async function loadSubject() {
|
|
5
|
+
return import("./web-auth.ts");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function authorization(username, password) {
|
|
9
|
+
return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test("enables password authentication only for a non-empty configured password", async () => {
|
|
13
|
+
const { isWebPasswordEnabled } = await loadSubject();
|
|
14
|
+
assert.equal(isWebPasswordEnabled(undefined), false);
|
|
15
|
+
assert.equal(isWebPasswordEnabled(""), false);
|
|
16
|
+
assert.equal(isWebPasswordEnabled("secret"), true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("accepts only the fixed pi username and configured password", async () => {
|
|
20
|
+
const { isValidBasicAuthorization } = await loadSubject();
|
|
21
|
+
assert.equal(isValidBasicAuthorization(authorization("pi", "secret"), "secret"), true);
|
|
22
|
+
assert.equal(isValidBasicAuthorization(authorization("admin", "secret"), "secret"), false);
|
|
23
|
+
assert.equal(isValidBasicAuthorization(authorization("pi", "wrong"), "secret"), false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("supports UTF-8 passwords and colons in the password", async () => {
|
|
27
|
+
const { isValidBasicAuthorization } = await loadSubject();
|
|
28
|
+
const password = "口令:with:colons";
|
|
29
|
+
assert.equal(isValidBasicAuthorization(authorization("pi", password), password), true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("rejects missing, malformed, and non-canonical authorization values", async () => {
|
|
33
|
+
const { isValidBasicAuthorization } = await loadSubject();
|
|
34
|
+
const valid = authorization("pi", "secret");
|
|
35
|
+
|
|
36
|
+
assert.equal(isValidBasicAuthorization(null, "secret"), false);
|
|
37
|
+
assert.equal(isValidBasicAuthorization("Bearer token", "secret"), false);
|
|
38
|
+
assert.equal(isValidBasicAuthorization("Basic !!!", "secret"), false);
|
|
39
|
+
assert.equal(isValidBasicAuthorization(`${valid}!`, "secret"), false);
|
|
40
|
+
assert.equal(isValidBasicAuthorization(
|
|
41
|
+
`Basic ${Buffer.from("missing-separator", "utf8").toString("base64")}`,
|
|
42
|
+
"secret",
|
|
43
|
+
), false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("does not authenticate when password protection is disabled", async () => {
|
|
47
|
+
const { isValidBasicAuthorization } = await loadSubject();
|
|
48
|
+
assert.equal(isValidBasicAuthorization(authorization("pi", ""), ""), false);
|
|
49
|
+
assert.equal(isValidBasicAuthorization(authorization("pi", "secret"), undefined), false);
|
|
50
|
+
});
|
package/lib/web-auth.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const PI_WEB_AUTH_USERNAME = "pi";
|
|
4
|
+
|
|
5
|
+
function hashSecret(value: string): Buffer {
|
|
6
|
+
return createHash("sha256").update(value, "utf8").digest();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function secretsEqual(actual: string, expected: string): boolean {
|
|
10
|
+
return timingSafeEqual(hashSecret(actual), hashSecret(expected));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isWebPasswordEnabled(
|
|
14
|
+
password: string | undefined = process.env.PI_WEB_PASSWORD,
|
|
15
|
+
): password is string {
|
|
16
|
+
return typeof password === "string" && password.length > 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isValidBasicAuthorization(
|
|
20
|
+
authorization: string | null,
|
|
21
|
+
password = process.env.PI_WEB_PASSWORD,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (!isWebPasswordEnabled(password) || !authorization) return false;
|
|
24
|
+
|
|
25
|
+
const match = /^Basic\s+(\S+)$/i.exec(authorization);
|
|
26
|
+
if (!match) return false;
|
|
27
|
+
|
|
28
|
+
let credentials: string;
|
|
29
|
+
try {
|
|
30
|
+
const decoded = Buffer.from(match[1], "base64");
|
|
31
|
+
if (decoded.toString("base64") !== match[1]) return false;
|
|
32
|
+
credentials = new TextDecoder("utf-8", { fatal: true }).decode(decoded);
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const separator = credentials.indexOf(":");
|
|
38
|
+
if (separator === -1) return false;
|
|
39
|
+
|
|
40
|
+
const username = credentials.slice(0, separator);
|
|
41
|
+
const suppliedPassword = credentials.slice(separator + 1);
|
|
42
|
+
const usernameMatches = secretsEqual(username, PI_WEB_AUTH_USERNAME);
|
|
43
|
+
const passwordMatches = secretsEqual(suppliedPassword, password);
|
|
44
|
+
return usernameMatches && passwordMatches;
|
|
45
|
+
}
|