From ad7e9d294dfb011e5e30d5409dd4207fd89bf569 Mon Sep 17 00:00:00 2001 From: terra tauri Date: Mon, 5 Oct 2026 12:31:06 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20pin=20delete=20=E2=80=94=20delete=20a?= =?UTF-8?q?=20share=20you=20made?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pin's end-to-end suite runs against production through this CLI and left every share it made behind (Operate's review of pin#43). pin delete sends DELETE /api/pins/{id}; the server lets only the share's owner delete it (bitcomplete/pin#43). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Sn3acTQsYfyvVidkmhCC5j --- delete_test.go | 36 +++++++++++++++++++++++++++ main.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 delete_test.go diff --git a/delete_test.go b/delete_test.go new file mode 100644 index 0000000..cdd5afc --- /dev/null +++ b/delete_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestDeleteShare(t *testing.T) { + var got struct{ method, path, auth string } + status, body := http.StatusNoContent, "" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got.method, got.path, got.auth = r.Method, r.URL.Path, r.Header.Get("Authorization") + w.WriteHeader(status) + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + if err := deleteShare(context.Background(), srv.Client(), srv.URL, "01HXYZ", "tok"); err != nil { + t.Fatalf("204: %v", err) + } + if got.method != http.MethodDelete || got.path != "/api/pins/01HXYZ" || got.auth != "Bearer tok" { + t.Fatalf("sent %+v", got) + } + + status = http.StatusNotFound + if err := deleteShare(context.Background(), srv.Client(), srv.URL, "01HXYZ", "tok"); err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("404: %v", err) + } + status, body = http.StatusForbidden, "only the person who shared it can delete it" + if err := deleteShare(context.Background(), srv.Client(), srv.URL, "01HXYZ", "tok"); err == nil || !strings.Contains(err.Error(), "http 403: only the person") { + t.Fatalf("403: %v", err) + } +} diff --git a/main.go b/main.go index d99c5dc..0dad0ef 100644 --- a/main.go +++ b/main.go @@ -63,6 +63,8 @@ func main() { os.Exit(runPublish(os.Args[2:])) case "unpublish": os.Exit(runUnpublish(os.Args[2:])) + case "delete": + os.Exit(runDelete(os.Args[2:])) case "components": os.Exit(runComponents(os.Args[2:])) case "whoami": @@ -90,6 +92,7 @@ Usage: capability link. --ttl sets the lifetime (default 7d, max 30d). Prints the public URL. pin unpublish Revoke a public link before it expires. + pin delete Delete a share you made, every form of it. pin components List MDX components, grouped by category. pin components get Show one component's props + example. pin components dump Print every component's full detail. @@ -702,6 +705,70 @@ func runUnpublish(args []string) int { return 0 } +// runDelete deletes a share the signed-in person made. Only its owner can: +// the server refuses anyone else (403), and a share made before pin recorded +// owners cannot be deleted this way. +func runDelete(args []string) int { + if len(args) != 1 { + fmt.Fprintln(os.Stderr, "usage: pin delete ") + return 2 + } + id, hostFromURL := parseShareRef(args[0]) + if id == "" { + fmt.Fprintf(os.Stderr, "pin delete: not a share id or URL: %q\n", args[0]) + return 2 + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + c, err := loadCreds() + if err != nil { + fmt.Fprintf(os.Stderr, "pin delete: not logged in. Run `pin login`.\n") + return 1 + } + c, err = ensureFreshAccess(ctx, c) + if err != nil { + fmt.Fprintf(os.Stderr, "pin delete: refresh: %v\n", err) + return 1 + } + + base := hostFromURL + if base == "" { + base = host() + } + if err := deleteShare(ctx, http.DefaultClient, base, id, c.AccessToken); err != nil { + fmt.Fprintf(os.Stderr, "pin delete: %v\n", err) + return 1 + } + fmt.Fprintf(os.Stderr, "deleted %s\n", id) + return 0 +} + +// deleteShare sends DELETE /api/pins/{id}; nil only on 204. +func deleteShare(ctx context.Context, client *http.Client, base, id, accessToken string) error { + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, base+"/api/pins/"+url.PathEscape(id), nil) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("X-Agent", agent()) + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusNoContent: + return nil + case http.StatusNotFound: + return fmt.Errorf("not found: %s", id) + default: + rbody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(rbody))) + } +} + // parsePublicRef extracts the capability token from either a bare token // or a full public URL like // "https://pin.bitcomplete.dev/public/p/{id}?token=…". Returns (token,