From 2e77daf7b7b63ea166c7e035a2c054ecf749a898 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:03:01 +0000 Subject: [PATCH] Document quota recovery and user-approved cleanup --- lib/discovery.test.ts | 16 ++++++++++++++ lib/skill-content.ts | 46 ++++++++++++++++++++++++++++++++++++++- skills/just-html/SKILL.md | 46 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/lib/discovery.test.ts b/lib/discovery.test.ts index 86222e6..3f32439 100644 --- a/lib/discovery.test.ts +++ b/lib/discovery.test.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { z } from "zod"; @@ -7,6 +8,9 @@ import { GET as getSkillIndex } from "@/app/.well-known/agent-skills/index.json/ import { GET as getSkill } from "@/app/.well-known/agent-skills/just-html/SKILL.md/route"; import { GET as getIntegrations } from "@/app/.well-known/integrations.json/route"; import { GET as getOpenApi } from "@/app/openapi.json/route"; +import { GET as getLlms } from "@/app/llms.txt/route"; +import { MAX_STORAGE_BYTES_PER_USER } from "@/lib/docs/config"; +import { LLMS_BODY, SKILL_MARKDOWN } from "@/lib/skill-content"; const basis = z .object({ @@ -127,6 +131,18 @@ describe("integration discovery", () => { expect(document.components.securitySchemes).toHaveProperty("bearerApiKey"); }); + it("serves quota recovery guidance in llms.txt and the published skill", async () => { + const response = getLlms(); + const body = await response.text(); + expect(response.headers.get("content-type")).toBe("text/plain; charset=utf-8"); + expect(body).toBe(LLMS_BODY); + expect(body).toContain('"limit_value":' + MAX_STORAGE_BYTES_PER_USER); + expect(body).toContain("scope=owned&limit=500"); + expect(body).toContain("Ask the user which specific docs to delete. Never delete automatically"); + expect(await getSkill().text()).toBe(SKILL_MARKDOWN); + expect(readFileSync("skills/just-html/SKILL.md", "utf8")).toBe(SKILL_MARKDOWN); + }); + it("publishes an Agent Skills index with a matching artifact digest", async () => { const indexResponse = getSkillIndex(); const skillResponse = getSkill(); diff --git a/lib/skill-content.ts b/lib/skill-content.ts index 976124e..09f3b12 100644 --- a/lib/skill-content.ts +++ b/lib/skill-content.ts @@ -268,12 +268,56 @@ Resource quotas (per user): Max HTML size per doc 2 MB request rejected 413 payload_too_large Docs per user 500 soft-deleted don't count; 403 quota_exceeded Versions retained per doc 100 oldest snapshots pruned beyond this - Total storage per user 100 MB current html + retained snapshots; 403 + Total storage per user 100 MiB current html + retained snapshots; 403 Grants per doc 50 403 quota_exceeded Comment body size 10 KB 413 payload_too_large Comments per doc 1,000 403 quota_exceeded API keys per user 10 +### Recovering from a storage or document-count quota + +A 403 with { "error":"quota_exceeded", "limit":"storage", +"limit_value":104857600, "current":... } means the write would exceed the +account's 100 MiB storage quota (current and limit_value are bytes). +limit:"doc_count" means the 500-document cap. This is not an authentication or +rate-limit failure: do not +re-register, repeatedly retry the write, or switch hosts without asking the user. +Keep the unpublished HTML locally while resolving the quota. + +1. Find cleanup candidates among OWNED docs, not shared docs or bookmarks: + curl -s 'https://justhtml.sh/api/v1/docs?scope=owned&limit=500' -H "Authorization: Bearer $JUSTHTML_API_KEY" + The default limit is only 100; 500 covers the full document quota. Results + arrive newest-created first. Sort locally by updated_at ascending to find + older docs; inspect titles and, where needed, GET /docs/:slug for context. + Look for superseded drafts, duplicate exports, temporary previews, or old + one-off reports. Age alone does not mean unused: updated_at measures edits, + not readership. Do not assume a shared or long-lived reference is disposable. + +2. Estimate the space each likely candidate would release. GET /docs/:slug + returns current html; GET /docs/:slug/versions returns versions[].bytes. + Add the UTF-8 byte length of current html to the sum of ALL retained snapshot + bytes (including the current version's snapshot). Both count toward quota. + The listing has no storage-size field. Shrinking HTML can add a snapshot, + so it is not a reliable way to reclaim space; there is no version-delete API. + +3. Offer 3-5 candidates (or fewer if fewer are suitable) as suggestions, with + title, URL/slug, last-updated date, estimated reclaimable MiB, and why each + looks safe to remove. Label estimates; never invent usage or readership data. + Ask the user which specific docs to delete. Never delete automatically, in + bulk, or solely because a doc is old. If no safe candidates emerge, say so + and ask whether the user prefers keeping everything or another hosting option. + +4. Only after explicit approval for the specific docs, offer to export any + content/history they want to keep and complete any requested export before + DELETE /docs/:slug for approved docs only. Deletion is soft but removes + viewer access, including existing share links; the doc's current HTML and + retained snapshots stop counting toward quota. Do not promise an API restore + operation. + +5. Retry the original publish/update once after cleanup. If it still hits the + quota, report that and suggest another small set for approval, not an + automatic deletion loop. Return the published URL when the write succeeds. + API rate limits (per API key) -> 429 with Retry-After + { error: "rate_limited" }: Doc creates 60 / hour Writes (PATCH,/edits,grants,rotate) 60 / min diff --git a/skills/just-html/SKILL.md b/skills/just-html/SKILL.md index fca2d7d..351dc7d 100644 --- a/skills/just-html/SKILL.md +++ b/skills/just-html/SKILL.md @@ -260,12 +260,56 @@ Resource quotas (per user): Max HTML size per doc 2 MB request rejected 413 payload_too_large Docs per user 500 soft-deleted don't count; 403 quota_exceeded Versions retained per doc 100 oldest snapshots pruned beyond this - Total storage per user 100 MB current html + retained snapshots; 403 + Total storage per user 100 MiB current html + retained snapshots; 403 Grants per doc 50 403 quota_exceeded Comment body size 10 KB 413 payload_too_large Comments per doc 1,000 403 quota_exceeded API keys per user 10 +### Recovering from a storage or document-count quota + +A 403 with { "error":"quota_exceeded", "limit":"storage", +"limit_value":104857600, "current":... } means the write would exceed the +account's 100 MiB storage quota (current and limit_value are bytes). +limit:"doc_count" means the 500-document cap. This is not an authentication or +rate-limit failure: do not +re-register, repeatedly retry the write, or switch hosts without asking the user. +Keep the unpublished HTML locally while resolving the quota. + +1. Find cleanup candidates among OWNED docs, not shared docs or bookmarks: + curl -s 'https://justhtml.sh/api/v1/docs?scope=owned&limit=500' -H "Authorization: Bearer $JUSTHTML_API_KEY" + The default limit is only 100; 500 covers the full document quota. Results + arrive newest-created first. Sort locally by updated_at ascending to find + older docs; inspect titles and, where needed, GET /docs/:slug for context. + Look for superseded drafts, duplicate exports, temporary previews, or old + one-off reports. Age alone does not mean unused: updated_at measures edits, + not readership. Do not assume a shared or long-lived reference is disposable. + +2. Estimate the space each likely candidate would release. GET /docs/:slug + returns current html; GET /docs/:slug/versions returns versions[].bytes. + Add the UTF-8 byte length of current html to the sum of ALL retained snapshot + bytes (including the current version's snapshot). Both count toward quota. + The listing has no storage-size field. Shrinking HTML can add a snapshot, + so it is not a reliable way to reclaim space; there is no version-delete API. + +3. Offer 3-5 candidates (or fewer if fewer are suitable) as suggestions, with + title, URL/slug, last-updated date, estimated reclaimable MiB, and why each + looks safe to remove. Label estimates; never invent usage or readership data. + Ask the user which specific docs to delete. Never delete automatically, in + bulk, or solely because a doc is old. If no safe candidates emerge, say so + and ask whether the user prefers keeping everything or another hosting option. + +4. Only after explicit approval for the specific docs, offer to export any + content/history they want to keep and complete any requested export before + DELETE /docs/:slug for approved docs only. Deletion is soft but removes + viewer access, including existing share links; the doc's current HTML and + retained snapshots stop counting toward quota. Do not promise an API restore + operation. + +5. Retry the original publish/update once after cleanup. If it still hits the + quota, report that and suggest another small set for approval, not an + automatic deletion loop. Return the published URL when the write succeeds. + API rate limits (per API key) -> 429 with Retry-After + { error: "rate_limited" }: Doc creates 60 / hour Writes (PATCH,/edits,grants,rotate) 60 / min