Keep existing TTL and password settings when push flags are omitted, and report the resulting policy in commit responses and CLI output.
- commit
- 930bc04a0db9b6e1cfc986bb0509da1d55a654f5
- parent
- b73fe3a8f8f4214a0d0411af6d15184e21407744
- author
- Dami <dami.etoile@gmail.com>
| M | cli/ | 6 | ++-- |
| M | cli/ | 13 | ++++++- |
| A | cli/ | 22 | +++++++++++ |
| M | shared/ | 4 | ++- |
| M | worker/ | 22 | ++++++++---- |
| M | worker/ | 2 | + |
| M | worker/ | 58 | ++++++++++++++++++++++++++++++ |
@@ -72,9 +72,9 @@ nzip
7272 Vault descriptions are included in `vault ls --json`. Use7373 `nzip vault update <name> --no-description` to clear one.7474 75-Password and TTL are committed with the content. On a new site, omitting `--password` creates an76-unprotected site; on an existing target, omission preserves its current password. Pass77-`--no-password` to clear protection explicitly.75+Password and TTL are committed with the content. On a new site, omitting `--ttl` uses 14 days and76+omitting `--password` creates an unprotected site. On an existing target, omitted settings preserve77+their current values. Pass `--no-password` to clear protection explicitly.7878 7979 `nzip site cp work:demo ./recovered-demo` reconstructs the current hosted bundle into an empty8080 directory. It uses the configured bearer token, verifies file hashes, and never exposes source via@@ -6,6 +6,15 @@ import { amber, bold, cyan, dim, emit, fail, green, parseTtl, ttlLeft } from "..
66 77 const UPLOAD_CONCURRENCY = 6;88 9+export function formatPushPolicy(10+ expiresAt: number | null,11+ protectedSite: boolean,12+): string {13+ const expiry = expiresAt === null ? "forever" : `expires in ${ttlLeft(expiresAt)}`;14+ const passwordStatus = protectedSite ? "password protected" : "no password";15+ return `${expiry}, ${passwordStatus}`;16+}17+918 export async function cmdPush(1019 config: Config,1120 path: string | undefined,@@ -100,12 +109,12 @@ export async function cmdPush(
100109 }).catch(() => {});101110 102111 const label = res.alias ? `${"vault" in target ? target.vault : "?"}:${res.alias}` : res.address;103- const expiry = res.expiresAt === null ? "forever" : `expires in ${ttlLeft(res.expiresAt)}`;112+ const policy = formatPushPolicy(res.expiresAt, res.protected);104113 emit(105114 () =>106115 console.log(107116 `${green("✓")} pushed ${bold(label)} → ${cyan(res.url)} ${108- dim(`(${expiry}, push #${res.seq})`)117+ dim(`(${policy}, push #${res.seq})`)109118 }`,110119 ),111120 {@@ -0,0 +1,22 @@
1+import { formatPushPolicy } from "./push.ts";2+3+function assertEquals(actual: string, expected: string): void {4+ if (actual !== expected) throw new Error(`expected ${expected}, got ${actual}`);5+}6+7+function assertStringIncludes(actual: string, expected: string): void {8+ if (!actual.includes(expected)) {9+ throw new Error(`expected ${JSON.stringify(actual)} to include ${JSON.stringify(expected)}`);10+ }11+}12+13+Deno.test("push feedback reports permanent password protection", () => {14+ assertEquals(formatPushPolicy(null, true), "forever, password protected");15+});16+17+Deno.test("push feedback reports finite expiry without password protection", () => {18+ assertStringIncludes(19+ formatPushPolicy(Math.floor(Date.now() / 1000) + 86_400, false),20+ "expires in 1d, no password",21+ );22+});@@ -64,7 +64,7 @@ export interface CommitRequest {
6464 manifest: Manifest;6565 /** Address or vault destination that should receive the new version. */6666 target: Target;67- /** Lifetime in days, `"forever"` for no expiry, or omitted for the server default. */67+ /** Lifetime in days, `"forever"` for no expiry, or omitted to preserve an existing value (14 days for a new site). */6868 ttl?: number | "forever";6969 /** Password to set, `null` to clear it, or omitted to preserve an existing value. */7070 password?: string | null;@@ -82,6 +82,8 @@ export interface CommitResponse {
8282 manifestHash: string;8383 /** Unix expiry timestamp in seconds, or `null` for a permanent site. */8484 expiresAt: number | null;85+ /** Whether visitors must unlock the committed site with a password. */86+ protected: boolean;8587 /** Monotonically increasing push sequence number within this site. */8688 seq: number;8789 }@@ -104,13 +104,13 @@ export async function handleCommit(
104104 );105105 }106106 107- const expiresAt = ttlToExpiry(body.ttl);108107 const target = body.target as Target;109108 let address: number;110109 let alias: string | null = null;111110 let isNew: boolean;111+ let existingSite: Awaited<ReturnType<typeof getSiteByAddress>> = null;112112 if ("address" in target) {113- const existing = await getSiteByAddress(env, target.address);113+ existingSite = await getSiteByAddress(env, target.address);114114 const slot = vaultSlotOf(target.address);115115 if (!(await getVaultBySlot(env, slot))) {116116 throw new ApiError(@@ -119,8 +119,8 @@ export async function handleCommit(
119119 );120120 }121121 address = target.address;122- alias = existing?.alias ?? null;123- isNew = !existing;122+ alias = existingSite?.alias ?? null;123+ isNew = !existingSite;124124 } else {125125 const vault = await getVaultByName(env, target.vault);126126 if (!vault) throw new ApiError(404, `unknown vault: ${target.vault}`);@@ -128,11 +128,11 @@ export async function handleCommit(
128128 if (alias !== null && !isValidName(alias)) {129129 throw new ApiError(400, `invalid alias: ${alias}`);130130 }131- const existing = alias === null131+ existingSite = alias === null132132 ? null133133 : await resolveTarget(env, { vault: target.vault, alias });134- if (existing) {135- address = existing.address;134+ if (existingSite) {135+ address = existingSite.address;136136 isNew = false;137137 } else {138138 address = await allocateAddress(env, vault.slot);@@ -140,6 +140,13 @@ export async function handleCommit(
140140 }141141 }142142 143+ const expiresAt = body.ttl === undefined && existingSite144+ ? existingSite.expires_at145+ : ttlToExpiry(body.ttl);146+ const protectedSite = passwordHash === undefined && existingSite147+ ? existingSite.password_hash !== null148+ : passwordHash !== null && passwordHash !== undefined;149+143150 if (!(await env.CONTENT.head(`manifest/${hash}`))) {144151 await env.CONTENT.put(`manifest/${hash}`, bytes);145152 }@@ -160,6 +167,7 @@ export async function handleCommit(
160167 alias,161168 manifestHash: hash,162169 expiresAt,170+ protected: protectedSite,163171 seq,164172 });165173 }@@ -200,6 +200,64 @@ describe("Worker runtime", () => {
200200 expect((await response.json<{ version: string }>()).version).toBe("0.7.1");201201 });202202 203+ it("preserves TTL and password settings when repushing without policy flags", async () => {204+ const address = 0xeffe;205+ const expiresAt = Math.floor(Date.now() / 1000) + 90 * 86_400;206+ const emptyHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";207+ const oldManifest = "a".repeat(64);208+ await env.DB.batch([209+ env.DB.prepare(210+ "INSERT INTO vaults (slot, name, created_at) VALUES (?, ?, ?)",211+ ).bind(14, "repush-policy", Math.floor(Date.now() / 1000)),212+ env.DB.prepare(213+ `INSERT INTO sites214+ (address, vault_slot, alias, current_manifest, created_at, updated_at, expires_at, password_hash)215+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,216+ ).bind(217+ address,218+ 14,219+ "keep-settings",220+ oldManifest,221+ Math.floor(Date.now() / 1000),222+ Math.floor(Date.now() / 1000),223+ expiresAt,224+ "existing-password-hash",225+ ),226+ ]);227+ await env.CONTENT.put(`blob/${emptyHash}`, new Uint8Array());228+229+ const response = await SELF.fetch("https://share.demo.dev/api/push/commit", {230+ method: "POST",231+ headers: {232+ authorization: "Bearer runtime-test-token",233+ "content-type": "application/json",234+ },235+ body: JSON.stringify({236+ manifest: {237+ v: 1,238+ files: {239+ "index.html": { h: emptyHash, s: 0, ct: "text/html; charset=utf-8" },240+ },241+ },242+ target: { address },243+ }),244+ });245+246+ expect(response.status).toBe(200);247+ expect(await response.json()).toMatchObject({248+ address: "effe",249+ expiresAt,250+ protected: true,251+ });252+ const stored = await env.DB.prepare(253+ "SELECT expires_at, password_hash FROM sites WHERE address = ?",254+ ).bind(address).first<{ expires_at: number | null; password_hash: string | null }>();255+ expect(stored).toEqual({256+ expires_at: expiresAt,257+ password_hash: "existing-password-hash",258+ });259+ });260+203261 it("creates, lists, renames, and redescribes vaults", async () => {204262 const headers = {205263 authorization: "Bearer runtime-test-token",