hub v0.128.0: set-aside deletion through the hub after a 7-day wait (decision 74, R-823), key-file clean-up route (R-826), read-only key check (R-827), window cap = half
gates / gates (push) Successful in 29s
gates / gates (push) Successful in 29s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -366,6 +366,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleOffsiteWindowOpen(w, r, strings.TrimPrefix(path, "/offsite/window-open/"))
|
||||
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/window-close/"):
|
||||
h.handleOffsiteWindowClose(w, r, strings.TrimPrefix(path, "/offsite/window-close/"))
|
||||
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/abandon-request/"):
|
||||
h.handleOffsiteAbandon(w, r, "request", strings.TrimPrefix(path, "/offsite/abandon-request/"))
|
||||
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/abandon-cancel/"):
|
||||
h.handleOffsiteAbandon(w, r, "cancel", strings.TrimPrefix(path, "/offsite/abandon-cancel/"))
|
||||
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/abandon-status/"):
|
||||
h.handleOffsiteAbandon(w, r, "status", strings.TrimPrefix(path, "/offsite/abandon-status/"))
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(path, "/artifacts/"):
|
||||
customerID := strings.TrimPrefix(path, "/artifacts/")
|
||||
h.handleArtifactManifest(w, r, customerID)
|
||||
|
||||
@@ -18,6 +18,9 @@ type OffsiteKeyService interface {
|
||||
MoveAside(ctx context.Context, customerID string) (string, error)
|
||||
OpenWindowFor(ctx context.Context, customerID string, countBefore int) (offsitekeys.WindowGrant, error)
|
||||
CloseWindowFor(ctx context.Context, customerID string, r offsitekeys.WindowResult) error
|
||||
RequestAbandon(ctx context.Context, customerID, path string) (offsitekeys.AbandonStatus, error)
|
||||
CancelAbandon(customerID, by string) (int, error)
|
||||
AbandonStatusFor(customerID string) (offsitekeys.AbandonStatus, error)
|
||||
}
|
||||
|
||||
// SetOffsiteKeyService wires the key registrar.
|
||||
@@ -166,3 +169,47 @@ func (h *Handler) handleOffsiteWindowClose(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"closed": true})
|
||||
}
|
||||
|
||||
// handleOffsiteAbandon: the box's half of decision 74 (R-823).
|
||||
//
|
||||
// POST /offsite/abandon-request/<id> {"path": "<repo>.orphaned-…"} → recorded; deleted by the hub after the delay
|
||||
// POST /offsite/abandon-cancel/<id> → every pending request cancelled
|
||||
// POST /offsite/abandon-status/<id> → {"state": none|pending|cancelled|deleted, …}
|
||||
func (h *Handler) handleOffsiteAbandon(w http.ResponseWriter, r *http.Request, verb, customerID string) {
|
||||
if !h.offsiteKeyAuth(w, r, customerID) {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
defer cancel()
|
||||
switch verb {
|
||||
case "request":
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
body, _ := io.ReadAll(io.LimitReader(r.Body, 4<<10))
|
||||
if err := json.Unmarshal(body, &req); err != nil || req.Path == "" {
|
||||
http.Error(w, "body must be {\"path\": \"…\"}", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
st, err := h.offsiteKeys.RequestAbandon(ctx, customerID, req.Path)
|
||||
if err != nil {
|
||||
offsiteKeyErr(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, st)
|
||||
case "cancel":
|
||||
n, err := h.offsiteKeys.CancelAbandon(customerID, "box")
|
||||
if err != nil {
|
||||
offsiteKeyErr(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"cancelled": n})
|
||||
default:
|
||||
st, err := h.offsiteKeys.AbandonStatusFor(customerID)
|
||||
if err != nil {
|
||||
offsiteKeyErr(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, st)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,13 @@ func (f *fakeKeySvc) OpenWindowFor(context.Context, string, int) (offsitekeys.Wi
|
||||
return offsitekeys.WindowGrant{Granted: false, Reason: "not due"}, f.err
|
||||
}
|
||||
func (f *fakeKeySvc) CloseWindowFor(context.Context, string, offsitekeys.WindowResult) error { return f.err }
|
||||
func (f *fakeKeySvc) RequestAbandon(context.Context, string, string) (offsitekeys.AbandonStatus, error) {
|
||||
return offsitekeys.AbandonStatus{State: "pending"}, f.err
|
||||
}
|
||||
func (f *fakeKeySvc) CancelAbandon(string, string) (int, error) { return 1, f.err }
|
||||
func (f *fakeKeySvc) AbandonStatusFor(string) (offsitekeys.AbandonStatus, error) {
|
||||
return offsitekeys.AbandonStatus{State: "none"}, f.err
|
||||
}
|
||||
func (f *fakeKeySvc) MoveAside(context.Context, string) (string, error) {
|
||||
return "/home/felhom-repo.orphaned-20261003", f.err
|
||||
}
|
||||
@@ -108,6 +115,9 @@ func TestOffsiteKeyEndpoints_AuthAndNoPasswordInAnyResponse(t *testing.T) {
|
||||
{"/api/v1/offsite/move-aside/c1", ``},
|
||||
{"/api/v1/offsite/window-open/c1", `{"count_before":3}`},
|
||||
{"/api/v1/offsite/window-close/c1", `{"window_id":1,"count_after":3,"outcome":"nothing"}`},
|
||||
{"/api/v1/offsite/abandon-request/c1", `{"path":"/home/felhom-repo.orphaned-20261004"}`},
|
||||
{"/api/v1/offsite/abandon-cancel/c1", ``},
|
||||
{"/api/v1/offsite/abandon-status/c1", ``},
|
||||
} {
|
||||
rr := post(c.path, "ckey", c.body)
|
||||
if rr.Code != http.StatusOK {
|
||||
|
||||
Reference in New Issue
Block a user