Files
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00

158 lines
4.5 KiB
Go

// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
package handler
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/certctl-io/certctl/internal/auth"
)
// resolveActor extracts the authenticated named-key identity from the request
// context for audit-trail attribution. Returns the named-key name when set by
// the auth middleware, or "api" as a safe sentinel when the auth middleware
// did not populate the context (e.g., AUTH_TYPE=none, or internal/system calls
// that bypass auth).
//
// Post-M-002: this is the single source of truth for handler-layer actor
// resolution. Handlers must NOT hardcode string literals like "api-key-user"
// or "api" — always go through this helper so the named-key identity flows to
// services and the audit trail.
func resolveActor(ctx context.Context) string {
if user := auth.GetUser(ctx); user != "" {
return user
}
return "api"
}
// PagedResponse represents a paginated API response.
type PagedResponse struct {
Data interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
}
// CursorPagedResponse represents a cursor-paginated API response.
type CursorPagedResponse struct {
Data interface{} `json:"data"`
Total int64 `json:"total"`
NextCursor string `json:"next_cursor,omitempty"`
PageSize int `json:"page_size"`
}
// ErrorResponse represents a standard error response.
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
RequestID string `json:"request_id,omitempty"`
}
// JSON writes a JSON response with the given status code and data.
func JSON(w http.ResponseWriter, status int, data interface{}) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(data)
}
// Error writes a JSON error response with the given status code and message.
func Error(w http.ResponseWriter, status int, message string) error {
errResp := ErrorResponse{
Error: http.StatusText(status),
Message: message,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(errResp)
}
// ErrorWithRequestID writes a JSON error response including a request ID.
func ErrorWithRequestID(w http.ResponseWriter, status int, message, requestID string) error {
errResp := ErrorResponse{
Error: http.StatusText(status),
Message: message,
RequestID: requestID,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(errResp)
}
// encodeCursor creates an opaque cursor token from a timestamp and ID.
func encodeCursor(createdAt time.Time, id string) string {
raw := createdAt.Format(time.RFC3339Nano) + ":" + id
return base64.URLEncoding.EncodeToString([]byte(raw))
}
// decodeCursor extracts a timestamp and ID from a cursor token.
// Kept as var assignment to suppress unused lint — will be used when
// cursor-based pagination is wired into list handlers.
var _ = func(cursor string) (time.Time, string, error) {
raw, err := base64.URLEncoding.DecodeString(cursor)
if err != nil {
return time.Time{}, "", fmt.Errorf("invalid cursor: %w", err)
}
parts := strings.SplitN(string(raw), ":", 2)
if len(parts) != 2 {
return time.Time{}, "", fmt.Errorf("invalid cursor format")
}
t, err := time.Parse(time.RFC3339Nano, parts[0])
if err != nil {
return time.Time{}, "", fmt.Errorf("invalid cursor timestamp: %w", err)
}
return t, parts[1], nil
}
// filterFields removes fields not in the allowed list from the response data.
// Works with both single objects and slices.
func filterFields(data interface{}, fields []string) interface{} {
if len(fields) == 0 {
return data
}
// Create field set for O(1) lookup
fieldSet := make(map[string]bool, len(fields))
for _, f := range fields {
fieldSet[f] = true
}
// Marshal to JSON, then unmarshal to generic structure
bytes, err := json.Marshal(data)
if err != nil {
return data
}
// Try as array first
var arr []map[string]interface{}
if err := json.Unmarshal(bytes, &arr); err == nil {
for i := range arr {
for key := range arr[i] {
if !fieldSet[key] {
delete(arr[i], key)
}
}
}
return arr
}
// Try as object
var obj map[string]interface{}
if err := json.Unmarshal(bytes, &obj); err == nil {
for key := range obj {
if !fieldSet[key] {
delete(obj, key)
}
}
return obj
}
return data
}