@danypops/pi-packed 0.2.1 → 0.3.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 +7 -6
- package/extension/src/index.ts +8 -0
- package/extension/src/packed.ts +5 -0
- package/extension/src/security-tui.ts +33 -0
- package/extension/src/tools.ts +24 -15
- package/package.json +1 -1
- package/src/cli.ts +21 -1
- package/src/client.ts +21 -0
- package/src/constants.ts +2 -1
- package/src/security.ts +34 -0
- package/src/service.ts +19 -1
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ inside the supervised Bun daemon.
|
|
|
11
11
|
```text
|
|
12
12
|
┌─ Pi extension (Node-compatible) ──────────────┐
|
|
13
13
|
│ pkg_search · pkg_info · pkg_install │
|
|
14
|
-
│ /packages ·
|
|
15
|
-
│
|
|
14
|
+
│ /packages · /packed security settings │
|
|
15
|
+
│ policy-driven approval · no Bun/SQLite access │
|
|
16
16
|
└──────────────────┬────────────────────────────┘
|
|
17
17
|
│ authenticated loopback HTTP
|
|
18
18
|
┌─ packed.service (Bun) ────────────────────────┐
|
|
@@ -37,9 +37,7 @@ packed installed --json
|
|
|
37
37
|
/packages
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Packages execute arbitrary code. `pkg_install` always
|
|
41
|
-
confirmation before the extension sends the mutation to the authenticated
|
|
42
|
-
daemon.
|
|
40
|
+
Packages execute arbitrary code. `pkg_install` uses daemon-owned security settings with secure default `installApproval: always`. Open `/packed` to choose **Always require approval** or explicitly opt out with **Never require approval**. The unsafe opt-out allows agent installs without an interactive UI. `/packages` remains the package browser.
|
|
43
41
|
|
|
44
42
|
## CLI
|
|
45
43
|
|
|
@@ -53,6 +51,7 @@ daemon.
|
|
|
53
51
|
| `packed catalog [--json]` | Inspect the local package index |
|
|
54
52
|
| `packed install <source> [--json]` | Authenticated daemon install for `npm:`, `git:`, or `https://` sources |
|
|
55
53
|
| `packed remove <name> [--json]` | Authenticated daemon removal by bare npm name |
|
|
54
|
+
| `packed security [always\|never] [--json]` | Read or set the install approval policy |
|
|
56
55
|
| `packed serve` | Run the loopback daemon |
|
|
57
56
|
| `packed service` | Print the systemd user unit |
|
|
58
57
|
| `packed version` | Print the package/service version |
|
|
@@ -77,13 +76,15 @@ The daemon listens on loopback only.
|
|
|
77
76
|
| `GET` | `/search?q=&limit=&offline=1` |
|
|
78
77
|
| `GET` | `/info?name=` |
|
|
79
78
|
| `GET` | `/installed` |
|
|
79
|
+
| `GET` | `/security` |
|
|
80
|
+
| `POST` | `/security` with `{ "installApproval": "always" | "never" }` |
|
|
80
81
|
| `GET` | `/updates` |
|
|
81
82
|
| `GET` | `/catalog` |
|
|
82
83
|
| `POST` | `/install` with `{ "source": "..." }` |
|
|
83
84
|
| `POST` | `/remove` with `{ "name": "..." }` |
|
|
84
85
|
|
|
85
86
|
State defaults to `~/.cache/pi-packed/` and contains `token`, `port`,
|
|
86
|
-
`updates.json`, and `packed.db`. Relevant environment variables:
|
|
87
|
+
`updates.json`, `security.json`, and `packed.db`. Relevant environment variables:
|
|
87
88
|
|
|
88
89
|
- `PI_PACKED_HOME`
|
|
89
90
|
- `PI_PACKED_PI_HOME`
|
package/extension/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { registerTools } from "./tools.js";
|
|
|
13
13
|
import { showPackages } from "./tui.js";
|
|
14
14
|
import { createNatives } from "./packed.js";
|
|
15
15
|
import { formatUpdateNotice } from "./model.js";
|
|
16
|
+
import { showPackedSettings } from "./security-tui.js";
|
|
16
17
|
|
|
17
18
|
// Async factory (pi awaits it): the seam creates authenticated daemon
|
|
18
19
|
// clients lazily. It never executes Bun-only adapters or opens SQLite.
|
|
@@ -20,6 +21,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
20
21
|
const natives = await createNatives();
|
|
21
22
|
registerTools(pi, natives);
|
|
22
23
|
|
|
24
|
+
pi.registerCommand("packed", {
|
|
25
|
+
description: "Configure pi-packed security settings",
|
|
26
|
+
handler: async (_args, ctx) => {
|
|
27
|
+
await showPackedSettings(ctx, natives);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
23
31
|
pi.registerCommand("packages", {
|
|
24
32
|
description: "Browse and manage installed Pi packages (pi-packed)",
|
|
25
33
|
handler: async (_args, ctx) => {
|
package/extension/src/packed.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { PackageDaemonPort as ClientPackageDaemonPort } from "../../src/client.ts";
|
|
10
10
|
import type { InstalledPkg, Pkg, PkgInfo, UpdateEntry } from "../../src/ports.ts";
|
|
11
|
+
import type { InstallApproval, SecuritySettings } from "../../src/security.ts";
|
|
11
12
|
|
|
12
13
|
export type { InstalledPkg, UpdateEntry };
|
|
13
14
|
export type PackageInfo = PkgInfo;
|
|
@@ -25,6 +26,8 @@ export interface Natives {
|
|
|
25
26
|
info(name: string): Promise<PackageInfo>;
|
|
26
27
|
installed(): Promise<InstalledPkg[]>;
|
|
27
28
|
updates(): Promise<UpdateEntry[]>;
|
|
29
|
+
security(): Promise<SecuritySettings>;
|
|
30
|
+
setInstallApproval(value: InstallApproval): Promise<SecuritySettings>;
|
|
28
31
|
install(source: string): Promise<string>;
|
|
29
32
|
remove(name: string): Promise<string>;
|
|
30
33
|
}
|
|
@@ -58,6 +61,8 @@ export async function createNatives(connect: PackageDaemonConnector = connectDef
|
|
|
58
61
|
info: (name) => call((daemon) => daemon.info(name)),
|
|
59
62
|
installed: () => call((daemon) => daemon.installed()),
|
|
60
63
|
updates: () => call((daemon) => daemon.updates()),
|
|
64
|
+
security: () => call((daemon) => daemon.security()),
|
|
65
|
+
setInstallApproval: (value) => call((daemon) => daemon.setInstallApproval(value)),
|
|
61
66
|
install: (source) => call((daemon) => daemon.install(source)),
|
|
62
67
|
remove: (name) => call((daemon) => daemon.remove(name)),
|
|
63
68
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { InstallApproval } from "../../src/security.ts";
|
|
3
|
+
import type { Natives } from "./packed.ts";
|
|
4
|
+
|
|
5
|
+
const OPTIONS: Array<{ value: InstallApproval; label: string }> = [
|
|
6
|
+
{ value: "always", label: "Always require approval (recommended)" },
|
|
7
|
+
{ value: "never", label: "Never require approval (unsafe opt-out)" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export async function showPackedSettings(ctx: ExtensionCommandContext, natives: Natives): Promise<void> {
|
|
11
|
+
if (!ctx.hasUI) {
|
|
12
|
+
ctx.ui.notify("/packed requires interactive mode", "warning");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const current = await natives.security();
|
|
17
|
+
const choice = await ctx.ui.select(
|
|
18
|
+
`Package install approval · current: ${current.installApproval}`,
|
|
19
|
+
[...OPTIONS.map(({ label }) => label), "Cancel"],
|
|
20
|
+
);
|
|
21
|
+
const selected = OPTIONS.find(({ label }) => label === choice);
|
|
22
|
+
if (!selected || selected.value === current.installApproval) return;
|
|
23
|
+
const updated = await natives.setInstallApproval(selected.value);
|
|
24
|
+
ctx.ui.notify(
|
|
25
|
+
updated.installApproval === "always"
|
|
26
|
+
? "Package installs now require confirmation."
|
|
27
|
+
: "Package install confirmation disabled. Packages can execute arbitrary code.",
|
|
28
|
+
updated.installApproval === "always" ? "info" : "warning",
|
|
29
|
+
);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
ctx.ui.notify(`packed security settings failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
32
|
+
}
|
|
33
|
+
}
|
package/extension/src/tools.ts
CHANGED
|
@@ -10,6 +10,28 @@ function text(t: string, details: Record<string, unknown> = {}) {
|
|
|
10
10
|
return { content: [{ type: "text" as const, text: t }], details };
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export async function installPackageWithPolicy(
|
|
14
|
+
source: string,
|
|
15
|
+
natives: Pick<Natives, "security" | "install">,
|
|
16
|
+
ctx: { hasUI: boolean; ui: { confirm(title: string, message: string): Promise<boolean> } },
|
|
17
|
+
) {
|
|
18
|
+
try {
|
|
19
|
+
const { installApproval } = await natives.security();
|
|
20
|
+
if (installApproval === "always") {
|
|
21
|
+
if (!ctx.hasUI) return text("pkg_install requires interactive approval; change installApproval in /packed to opt out.");
|
|
22
|
+
const ok = await ctx.ui.confirm(
|
|
23
|
+
"Install Pi package",
|
|
24
|
+
`Run: pi install ${source}\n\nPackages execute arbitrary code. Continue?`,
|
|
25
|
+
);
|
|
26
|
+
if (!ok) return text("Install cancelled by user.");
|
|
27
|
+
}
|
|
28
|
+
const out = await natives.install(source);
|
|
29
|
+
return text(out || `Installed ${source}. Reload with /reload to activate.`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return text(`install failed: ${error instanceof Error ? error.message : error}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
export function registerTools(pi: ExtensionAPI, natives: Natives): void {
|
|
14
36
|
pi.registerTool({
|
|
15
37
|
name: "pkg_search",
|
|
@@ -71,25 +93,12 @@ export function registerTools(pi: ExtensionAPI, natives: Natives): void {
|
|
|
71
93
|
label: "Pi Package Install",
|
|
72
94
|
description:
|
|
73
95
|
"Install a Pi package (pi install). Supports npm:<pkg>[@ver], git:<host>/<owner>/<repo>[@ref], " +
|
|
74
|
-
"or https:// URLs. Packages execute arbitrary code
|
|
96
|
+
"or https:// URLs. Packages execute arbitrary code. Approval follows the installApproval policy configured in /packed (secure default: always).",
|
|
75
97
|
parameters: Type.Object({
|
|
76
98
|
source: Type.String({ description: "e.g. 'npm:pi-lsp', 'npm:@scope/pkg@1.2.3', 'git:github.com/u/r@v1'" }),
|
|
77
99
|
}),
|
|
78
100
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
79
|
-
|
|
80
|
-
return text("pkg_install requires an interactive session (the user must confirm).");
|
|
81
|
-
}
|
|
82
|
-
const ok = await ctx.ui.confirm(
|
|
83
|
-
"Install Pi package",
|
|
84
|
-
`Run: pi install ${params.source}\n\nPackages execute arbitrary code. Continue?`,
|
|
85
|
-
);
|
|
86
|
-
if (!ok) return text("Install cancelled by user.");
|
|
87
|
-
try {
|
|
88
|
-
const out = await natives.install(params.source);
|
|
89
|
-
return text(out || `Installed ${params.source}. Reload with /reload to activate.`);
|
|
90
|
-
} catch (e) {
|
|
91
|
-
return text(`install failed: ${e instanceof Error ? e.message : e}`);
|
|
92
|
-
}
|
|
101
|
+
return installPackageWithPolicy(params.source, natives, ctx);
|
|
93
102
|
},
|
|
94
103
|
});
|
|
95
104
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { checkUpdates } from "./watcher.ts";
|
|
|
11
11
|
import { syncCatalog } from "./catalog.ts";
|
|
12
12
|
import { openDb, searchLocal, catalogList, getSyncMeta, latestVersion, dbPath } from "./db.ts";
|
|
13
13
|
import { NAME_RE, defaultPiBin } from "./install.ts";
|
|
14
|
+
import type { InstallApproval, SecuritySettingsPort } from "./security.ts";
|
|
14
15
|
|
|
15
16
|
function defaultPiBinForUnit(): string | undefined {
|
|
16
17
|
const b = defaultPiBin();
|
|
@@ -33,6 +34,7 @@ usage:
|
|
|
33
34
|
packed catalog [--json] local package index (apt-cache stats)
|
|
34
35
|
packed install <source> [--json] pi install npm:|git:|https://… via daemon
|
|
35
36
|
packed remove <name> [--json] remove by bare npm name via daemon
|
|
37
|
+
packed security [always|never] [--json] read or set install approval policy
|
|
36
38
|
packed serve run the long-running daemon
|
|
37
39
|
packed service print a systemd user unit
|
|
38
40
|
packed version print version
|
|
@@ -41,6 +43,7 @@ usage:
|
|
|
41
43
|
export interface CliDeps {
|
|
42
44
|
reg: Registry;
|
|
43
45
|
inst: Installer;
|
|
46
|
+
security: SecuritySettingsPort;
|
|
44
47
|
stateDir: string;
|
|
45
48
|
piHome: string;
|
|
46
49
|
execPath?: string; // bun binary (defaults to process.execPath)
|
|
@@ -199,6 +202,22 @@ const commands: Record<string, { usage: string; run: Command }> = {
|
|
|
199
202
|
},
|
|
200
203
|
},
|
|
201
204
|
|
|
205
|
+
security: {
|
|
206
|
+
usage: "packed security [always|never] [--json]",
|
|
207
|
+
async run(_rest, d, flags, pos) {
|
|
208
|
+
const requested = pos[0];
|
|
209
|
+
if (requested !== undefined && requested !== "always" && requested !== "never") {
|
|
210
|
+
return usageErr(`usage: ${commands["security"]!.usage}\n`);
|
|
211
|
+
}
|
|
212
|
+
const settings = requested
|
|
213
|
+
? await d.security.setInstallApproval(requested as InstallApproval)
|
|
214
|
+
: await d.security.security();
|
|
215
|
+
return flags.json
|
|
216
|
+
? ok(`${JSON.stringify(settings)}\n`)
|
|
217
|
+
: ok(`install approval: ${settings.installApproval}\n`);
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
|
|
202
221
|
remove: {
|
|
203
222
|
usage: "packed remove <name> [--json] (bare npm name, e.g. pi-lsp or @scope/pkg)",
|
|
204
223
|
async run(_rest, d, flags, pos) {
|
|
@@ -277,7 +296,7 @@ if (import.meta.main) {
|
|
|
277
296
|
} else {
|
|
278
297
|
const { stateDir } = await import("./state.ts");
|
|
279
298
|
const { defaultPiHome } = await import("./installed.ts");
|
|
280
|
-
const { DaemonBackedInstaller, resolveRegistry } = await import("./client.ts");
|
|
299
|
+
const { DaemonBackedSecurity, DaemonBackedInstaller, resolveRegistry } = await import("./client.ts");
|
|
281
300
|
const dir = stateDir();
|
|
282
301
|
// mirror talks to UPSTREAM, not the daemon cache — apt update semantics.
|
|
283
302
|
const reg =
|
|
@@ -287,6 +306,7 @@ if (import.meta.main) {
|
|
|
287
306
|
const { code, out } = await cliRun(args, {
|
|
288
307
|
reg,
|
|
289
308
|
inst: new DaemonBackedInstaller(dir),
|
|
309
|
+
security: new DaemonBackedSecurity(dir),
|
|
290
310
|
stateDir: dir,
|
|
291
311
|
piHome: defaultPiHome(),
|
|
292
312
|
});
|
package/src/client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import type { InstalledPkg, Installer, PkgInfo, Registry, SearchPage, UpdateEntry, UpdatesSnapshot } from "./ports.ts";
|
|
9
9
|
import { HttpRegistry } from "./registry.ts";
|
|
10
10
|
import { DAEMON_HOST, PROBE_TIMEOUT_MS, REGISTRY_FETCH_TIMEOUT_MS, PORT_FILE, TOKEN_FILE } from "./constants.ts";
|
|
11
|
+
import type { InstallApproval, SecuritySettings } from "./security.ts";
|
|
11
12
|
|
|
12
13
|
export type FetchTransport = (request: Request) => Promise<Response>;
|
|
13
14
|
|
|
@@ -16,6 +17,8 @@ export interface PackageDaemonPort {
|
|
|
16
17
|
info(name: string): Promise<PkgInfo>;
|
|
17
18
|
installed(): Promise<InstalledPkg[]>;
|
|
18
19
|
updates(): Promise<UpdateEntry[]>;
|
|
20
|
+
security(): Promise<SecuritySettings>;
|
|
21
|
+
setInstallApproval(value: InstallApproval): Promise<SecuritySettings>;
|
|
19
22
|
install(source: string): Promise<string>;
|
|
20
23
|
remove(name: string): Promise<string>;
|
|
21
24
|
}
|
|
@@ -85,6 +88,14 @@ export class PackageDaemonClient implements PackageDaemonPort {
|
|
|
85
88
|
return (await this.request<UpdatesSnapshot>("/updates")).updates;
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
security(): Promise<SecuritySettings> {
|
|
92
|
+
return this.request("/security");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setInstallApproval(installApproval: InstallApproval): Promise<SecuritySettings> {
|
|
96
|
+
return this.request("/security", { method: "POST", body: JSON.stringify({ installApproval }) });
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
async install(source: string): Promise<string> {
|
|
89
100
|
const result = await this.request<MutationResponse>("/install", {
|
|
90
101
|
method: "POST",
|
|
@@ -119,6 +130,16 @@ export class PackageDaemonInstaller implements Installer {
|
|
|
119
130
|
}
|
|
120
131
|
}
|
|
121
132
|
|
|
133
|
+
export class DaemonBackedSecurity {
|
|
134
|
+
constructor(private readonly stateDirectory: string) {}
|
|
135
|
+
async security(): Promise<SecuritySettings> {
|
|
136
|
+
return (await connectPackageDaemon(this.stateDirectory)).security();
|
|
137
|
+
}
|
|
138
|
+
async setInstallApproval(value: InstallApproval): Promise<SecuritySettings> {
|
|
139
|
+
return (await connectPackageDaemon(this.stateDirectory)).setInstallApproval(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
export class DaemonBackedInstaller implements Installer {
|
|
123
144
|
constructor(private readonly stateDirectory: string) {}
|
|
124
145
|
|
package/src/constants.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const IDLE_BUDGET_DEFAULT_MS = 10 * 60_000; // on-demand self-exit
|
|
|
31
31
|
export const WATCHDOG_TICK_MS = 15_000;
|
|
32
32
|
|
|
33
33
|
// --- Identity ---
|
|
34
|
-
export const VERSION = "0.
|
|
34
|
+
export const VERSION = "0.3.0";
|
|
35
35
|
|
|
36
36
|
// --- State-dir file names ---
|
|
37
37
|
export const TOKEN_FILE = "token";
|
|
@@ -39,6 +39,7 @@ export const PORT_FILE = "port";
|
|
|
39
39
|
export const UPDATES_FILE = "updates.json";
|
|
40
40
|
export const DB_FILE = "packed.db";
|
|
41
41
|
export const SETTINGS_FILE = "settings.json";
|
|
42
|
+
export const SECURITY_FILE = "security.json";
|
|
42
43
|
|
|
43
44
|
// --- Environment knobs ---
|
|
44
45
|
export const ENV = {
|
package/src/security.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SECURITY_FILE } from "./constants.ts";
|
|
4
|
+
|
|
5
|
+
export const INSTALL_APPROVAL_VALUES = ["always", "never"] as const;
|
|
6
|
+
export type InstallApproval = typeof INSTALL_APPROVAL_VALUES[number];
|
|
7
|
+
export interface SecuritySettings { installApproval: InstallApproval }
|
|
8
|
+
export interface SecuritySettingsPort {
|
|
9
|
+
security(): Promise<SecuritySettings>;
|
|
10
|
+
setInstallApproval(value: InstallApproval): Promise<SecuritySettings>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_SECURITY_SETTINGS: SecuritySettings = { installApproval: "always" };
|
|
14
|
+
|
|
15
|
+
export function readSecuritySettings(stateDir: string): SecuritySettings {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(readFileSync(join(stateDir, SECURITY_FILE), "utf8")) as { installApproval?: unknown };
|
|
18
|
+
return INSTALL_APPROVAL_VALUES.includes(value.installApproval as InstallApproval)
|
|
19
|
+
? { installApproval: value.installApproval as InstallApproval }
|
|
20
|
+
: { ...DEFAULT_SECURITY_SETTINGS };
|
|
21
|
+
} catch {
|
|
22
|
+
return { ...DEFAULT_SECURITY_SETTINGS };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeSecuritySettings(stateDir: string, settings: SecuritySettings): SecuritySettings {
|
|
27
|
+
if (!INSTALL_APPROVAL_VALUES.includes(settings.installApproval)) throw new Error("installApproval must be always or never");
|
|
28
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
29
|
+
const target = join(stateDir, SECURITY_FILE);
|
|
30
|
+
const temporary = `${target}.tmp`;
|
|
31
|
+
writeFileSync(temporary, `${JSON.stringify(settings)}\n`, { mode: 0o600 });
|
|
32
|
+
renameSync(temporary, target);
|
|
33
|
+
return { ...settings };
|
|
34
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { readInstalledPackages, defaultPiHome } from "./installed.ts";
|
|
|
11
11
|
import { openDb, searchLocal, catalogList, getSyncMeta, dbPath } from "./db.ts";
|
|
12
12
|
import { VERSION, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT } from "./constants.ts";
|
|
13
13
|
import { createLogger } from "./log.ts";
|
|
14
|
+
import { readSecuritySettings, writeSecuritySettings, type InstallApproval } from "./security.ts";
|
|
14
15
|
|
|
15
16
|
const log = createLogger("service");
|
|
16
17
|
|
|
@@ -45,6 +46,23 @@ export function createApp(deps: Deps): { fetch: (req: Request) => Promise<Respon
|
|
|
45
46
|
return json({ ok: true, version: VERSION });
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
if (path === "/security" && req.method === "GET") {
|
|
50
|
+
return json(readSecuritySettings(deps.stateDir));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (path === "/security" && req.method === "POST") {
|
|
54
|
+
let installApproval: unknown;
|
|
55
|
+
try {
|
|
56
|
+
installApproval = ((await req.json()) as { installApproval?: unknown }).installApproval;
|
|
57
|
+
} catch {
|
|
58
|
+
return err(400, "invalid security settings JSON");
|
|
59
|
+
}
|
|
60
|
+
if (installApproval !== "always" && installApproval !== "never") {
|
|
61
|
+
return err(400, "installApproval must be always or never");
|
|
62
|
+
}
|
|
63
|
+
return json(writeSecuritySettings(deps.stateDir, { installApproval: installApproval as InstallApproval }));
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
if (path === "/search" && req.method === "GET") {
|
|
49
67
|
const q = url.searchParams.get("q") ?? "";
|
|
50
68
|
const limit = clampLimit(Number(url.searchParams.get("limit")), SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT);
|
|
@@ -143,7 +161,7 @@ export function createApp(deps: Deps): { fetch: (req: Request) => Promise<Respon
|
|
|
143
161
|
return err(401, "missing or invalid bearer token");
|
|
144
162
|
}
|
|
145
163
|
// Cache successful GETs by URI (smart-proxy concern).
|
|
146
|
-
if (req.method === "GET" && !["/health", "/updates", "/catalog"].includes(new URL(req.url).pathname)) {
|
|
164
|
+
if (req.method === "GET" && !["/health", "/updates", "/catalog", "/security"].includes(new URL(req.url).pathname)) {
|
|
147
165
|
const hit = cache.get(req.url);
|
|
148
166
|
if (hit) {
|
|
149
167
|
log.debug("request", { path: new URL(req.url).pathname, cache: "hit", ms: Date.now() - t0 });
|