- commit
- cdcb2154b8e80e903fdf1c04ff9a6646437fb6df
- parent
- 91422bb7445893f3e2d25dfb2d630ca016988547
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 6 | ++-- |
| M | cli/ | 14 | ++++--- |
| M | cli/ | 12 | +++++-- |
| A | cli/ | 55 | ++++++++++++++++++++++++++++++ |
| M | cli/ | 19 | ++++++++--- |
@@ -65,10 +65,10 @@ Commands accept any of three target forms:
6565 self-cleans: expired entries drop on write, `rm` forgets its entry, and `ls`6666 reconciles against the live set. Use it as `cd "$(nzip where personal:plan)"`.6767 - **Vault guardrail.** An optional `"allowVaults": ["home"]` in `config.json`68- restricts which vaults this install may target by name. Pushes or aliases69- outside the list are refused before any upload, so a home-project agent can't68+ restricts which vaults this install may target. Targets outside the list and69+ raw hex addresses are refused before any upload, so a home-project agent can't7070 drop a doc into a vault that sits adjacent to what you share professionally.71- Absent = unrestricted; raw hex addresses bypass it (they name no vault).71+ Absent = unrestricted.7272 7373 ## Commands7474 @@ -3,7 +3,7 @@
33 // so it stays fast enough for `cd "$(nzip where personal:plan)"`.44 55 import { resolveCliTarget } from "../lib/api.ts";6-import type { Config } from "../lib/config.ts";6+import { assertRawAddressAllowed, type Config } from "../lib/config.ts";77 import { lookup } from "../lib/paths.ts";88 import { amber, dim, emit, fail } from "../lib/fmt.ts";99 @@ -13,15 +13,17 @@ export async function cmdWhere(
1313 ): Promise<void> {1414 if (!raw) fail("usage: nzip where <target>");1515 16- const q = /^[0-9a-f]{4}$/.test(raw) ? { address: raw } : (() => {17- let resolved: string;16+ const q = (() => {1817 try {19- resolved = resolveCliTarget(raw, config);18+ if (/^[0-9a-f]{4}$/.test(raw)) {19+ assertRawAddressAllowed(config);20+ return { address: raw };21+ }22+ const [vault, alias] = resolveCliTarget(raw, config).split(":");23+ return { vault, alias };2024 } catch (e) {2125 return fail((e as Error).message);2226 }23- const [vault, alias] = resolved.split(":");24- return { vault, alias };2527 })();2628 2729 const entry = await lookup(q);@@ -13,7 +13,7 @@ import type {
1313 Target,1414 VaultInfo,1515 } from "@nzip/shared";16-import { assertVaultAllowed, type Config } from "./config.ts";16+import { assertRawAddressAllowed, assertVaultAllowed, type Config } from "./config.ts";1717 1818 export class ApiClient {1919 constructor(private config: Config) {}@@ -165,7 +165,10 @@ export class ApiClient {
165165 * A bare alias picks up the default vault from config.166166 */167167 export function resolveCliTarget(raw: string, config: Config): string {168- if (/^[0-9a-f]{4}$/.test(raw)) return raw; // raw address names no vault168+ if (/^[0-9a-f]{4}$/.test(raw)) {169+ assertRawAddressAllowed(config);170+ return raw;171+ }169172 if (raw.includes(":")) {170173 const [vault] = raw.split(":");171174 assertVaultAllowed(vault, config);@@ -191,7 +194,10 @@ export function commitTargetFor(
191194 assertVaultAllowed(vault, config);192195 return { vault };193196 }194- if (/^[0-9a-f]{4}$/.test(raw)) return { address: parseInt(raw, 16) };197+ if (/^[0-9a-f]{4}$/.test(raw)) {198+ assertRawAddressAllowed(config);199+ return { address: parseInt(raw, 16) };200+ }195201 const resolved = resolveCliTarget(raw, config);196202 const [vault, alias] = resolved.split(":");197203 return { vault, alias };@@ -0,0 +1,55 @@
1+import { commitTargetFor, resolveCliTarget } from "./api.ts";2+import type { Config } from "./config.ts";3+4+const unrestricted: Config = {5+ server: "https://example.com",6+ token: "test",7+};8+9+const restricted: Config = {10+ ...unrestricted,11+ allowVaults: ["home"],12+};13+14+function assertEquals(actual: unknown, expected: unknown): void {15+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {16+ throw new Error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);17+ }18+}19+20+function assertThrows(fn: () => unknown, message: string): void {21+ try {22+ fn();23+ } catch (error) {24+ if (error instanceof Error && error.message.includes(message)) return;25+ throw error;26+ }27+ throw new Error(`expected error containing ${JSON.stringify(message)}`);28+}29+30+Deno.test("raw addresses remain available without allowVaults", () => {31+ assertEquals(resolveCliTarget("2a3f", unrestricted), "2a3f");32+ assertEquals(commitTargetFor("2a3f", unrestricted), { address: 0x2a3f });33+});34+35+Deno.test("resolveCliTarget refuses a raw address when allowVaults is set", () => {36+ assertThrows(37+ () => resolveCliTarget("2a3f", restricted),38+ "raw address targets are not allowed by this config because allowVaults is set",39+ );40+});41+42+Deno.test("commitTargetFor refuses a raw address when allowVaults is set", () => {43+ assertThrows(44+ () => commitTargetFor("2a3f", restricted),45+ "raw address targets are not allowed by this config because allowVaults is set",46+ );47+});48+49+Deno.test("allowVaults still accepts an allowed named target", () => {50+ assertEquals(resolveCliTarget("home:demo", restricted), "home:demo");51+ assertEquals(commitTargetFor("home:demo", restricted), {52+ vault: "home",53+ alias: "demo",54+ });55+});@@ -6,11 +6,12 @@ export interface Config {
66 token: string;77 defaultVault?: string;88 /**9- * Optional allow-list of vault names this config may target by name. When10- * present, any push/site/rm/revert/default that names a vault outside the11- * list is refused — so a restricted agent can't drop content into a vault12- * that sits adjacent to things you share professionally. Absent = no limit;13- * empty array = no named vault is permitted.9+ * Optional allow-list of vault names this config may target. When present,10+ * any push/site/rm/revert/default that names a vault outside the list, or11+ * uses a raw address whose vault cannot be validated, is refused — so a12+ * restricted agent can't drop content into a vault that sits adjacent to13+ * things you share professionally. Absent = no limit; empty array = no14+ * target is permitted.1415 */1516 allowVaults?: string[];1617 }@@ -26,6 +27,14 @@ export function assertVaultAllowed(vault: string, config: Config): void {
2627 }2728 }2829 30+/** Throw when a raw address would bypass a configured vault allow-list. */31+export function assertRawAddressAllowed(config: Config): void {32+ if (!config.allowVaults) return; // unset → unrestricted33+ throw new Error(34+ "raw address targets are not allowed by this config because allowVaults is set; use an allowed vault alias",35+ );36+}37+2938 export function configDir(): string {3039 const xdg = Deno.env.get("XDG_CONFIG_HOME");3140 const home = Deno.env.get("HOME") ?? ".";