Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d9e3d4c
fix(api): key per-user rate limiter on JWT context keys
ericfitz Oct 18, 2026
a737626
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
fecf1c7
test(api): add per-user API rate limit integration workflow
ericfitz Oct 18, 2026
541bb1f
test(api): check X-RateLimit-Remaining only on allowed requests
ericfitz Oct 18, 2026
373e5eb
fix(api): map client credential create constraint error to documented…
ericfitz Oct 18, 2026
8ea01a5
style(api): use American spelling in the credential create log line
ericfitz Oct 18, 2026
efffe3d
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
075264e
fix(api): honor is_confidential on triage survey response list
ericfitz Oct 18, 2026
06c141b
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
480b6ca
fix(websocket): honor group-based grants in collaboration access checks
ericfitz Oct 18, 2026
9d7bdf1
fix(websocket): resolve participant permissions with participant groups
ericfitz Oct 18, 2026
e387172
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
c9de6d5
fix(dbtool): apply settings semantic validators before writing system…
ericfitz Oct 18, 2026
c2ed26c
fix(dbtool): validate timmy.llm_max_tokens and run validation on dry-run
ericfitz Oct 18, 2026
0f5bd93
fix(dbtool): clarify validation error does not imply write atomicity
ericfitz Oct 18, 2026
cd6387b
docs(dbtool): correct the comment on what setting validation errors c…
ericfitz Oct 18, 2026
245d7c3
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
9bb7fad
fix(cats): seed a decoy diagram so DELETE does not consume the diagra…
ericfitz Oct 18, 2026
87b58e1
fix(cats): decoy the survey anchor and name the seeded repository
ericfitz Oct 18, 2026
3e8d788
fix(cats): retarget repository metadata to the renamed repository
ericfitz Oct 18, 2026
bceea3e
fix(cats): report every manifest fixture in the post-run survival check
ericfitz Oct 18, 2026
d5b0497
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
5fcb73d
chore(sem): drop superseded SEM@0 markers in the CATS reference writer
ericfitz Oct 18, 2026
e429383
fix(api): validate survey template status on create and update
ericfitz Oct 18, 2026
f690e00
fix(api): enum-check survey status query param and fix stale filter text
ericfitz Oct 18, 2026
63ad40d
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
9f8b0ba
fix(api): reject root-path JSON Patch operations
ericfitz Oct 18, 2026
83f9f21
test(api): cover root-path JSON Patch rejection at handler level
ericfitz Oct 18, 2026
af5c0be
fix(api): forbid root path and document from in JSON Patch schema
ericfitz Oct 18, 2026
a1a6ca0
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
dbd53fe
fix(api): reject JSON Patch move/copy from prohibited paths
ericfitz Oct 18, 2026
03ce26e
fix(api): validate all bulk threat patches before applying any
ericfitz Oct 18, 2026
3211485
fix(api): honor move/copy "from" in remaining JSON Patch guards
ericfitz Oct 18, 2026
befd6e3
fix(api): reject removal of survey response status via JSON Patch
ericfitz Oct 18, 2026
0f9a718
chore(sem): refresh SEM markers for changed functions
ericfitz Oct 18, 2026
31c24f7
test(cats): classify request_access 409 on a non-pending document as …
ericfitz Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions api-schema/tmi-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -6273,8 +6273,8 @@
},
"path": {
"type": "string",
"description": "JSON path to target",
"pattern": "^(/[^/]*)*$",
"description": "JSON Pointer (RFC 6901) to the target; must start with \"/\" (the document root \"\" is not allowed)",
"pattern": "^(/[^/]*)+$",
"maxLength": 512
},
"value": {
Expand Down Expand Up @@ -6322,6 +6322,12 @@
}
}
]
},
"from": {
"type": "string",
"description": "JSON Pointer (RFC 6901) to the source location. Required for move and copy operations, ignored otherwise; must start with \"/\".",
"pattern": "^(/[^/]*)+$",
"maxLength": 512
}
},
"required": [
Expand Down Expand Up @@ -8243,7 +8249,13 @@
"maxLength": 128,
"pattern": "^[^\\x00-\\x1F]*$",
"description": "Survey status: active surveys appear in intake, inactive are hidden but editable, archived are read-only and preserved for historical reference",
"nullable": true
"nullable": true,
"enum": [
"active",
"inactive",
"archived"
],
"x-go-type": "string"
},
"settings": {
"$ref": "#/components/schemas/SurveySettings"
Expand Down Expand Up @@ -14391,7 +14403,7 @@
"IsConfidentialQueryParam": {
"name": "is_confidential",
"in": "query",
"description": "Filter by secret_project flag",
"description": "Filter survey responses by their is_confidential flag, intersected with the caller's reviewer scope",
"schema": {
"type": "boolean"
}
Expand All @@ -14412,7 +14424,13 @@
"schema": {
"type": "string",
"maxLength": 128,
"pattern": "^[^\\x00-\\x1F]*$"
"pattern": "^[^\\x00-\\x1F]*$",
"enum": [
"active",
"inactive",
"archived"
],
"x-go-type": "string"
}
},
"SurveyId": {
Expand Down
4,112 changes: 2,065 additions & 2,047 deletions api/api.go

Large diffs are not rendered by default.

64 changes: 35 additions & 29 deletions api/client_credentials_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

// CreateCurrentUserClientCredential handles POST /me/client_credentials
// Creates a new OAuth 2.0 client credential for machine-to-machine authentication
// SEM@32e22d40fa43dfa14fa39b14713e41c740ebe026: build a client credential for the authenticated user, enforcing authorization, quota, and direct_write rules (writes DB)
// SEM@fd749f0ab4b11da6219ce6594a51301843131dc9: build a client credential for the authenticated user, enforcing quota and conflict rules (writes DB)
func (s *Server) CreateCurrentUserClientCredential(c *gin.Context) {
logger := slogging.Get().WithContext(c)

Expand Down Expand Up @@ -161,32 +161,7 @@ func (s *Server) CreateCurrentUserClientCredential(c *gin.Context) {
ExpiresAt: timeFromPtr(req.ExpiresAt),
})
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
logger.Debug("Request cancelled during credential create: %v", err)
return
}
if errors.Is(err, dberrors.ErrDuplicate) || errors.Is(err, dberrors.ErrConstraint) {
logger.Warn("Client credential creation failed due to constraint: %v", err)
c.JSON(http.StatusConflict, Error{
Error: ErrorError(errcode.Conflict),
ErrorDescription: "A client credential with these details already exists",
})
return
}
if errors.Is(err, dberrors.ErrTransient) {
logger.Warn("Transient DB error creating client credential: %v", err)
c.Header("Retry-After", "30")
c.JSON(http.StatusServiceUnavailable, Error{
Error: ErrorError(errcode.ServiceUnavailable),
ErrorDescription: "Database temporarily unavailable, please retry",
})
return
}
logger.Error("Failed to create client credential for user %s: %v", ownerUUID, err)
c.JSON(http.StatusInternalServerError, Error{
Error: ErrorError(errcode.ServerError),
ErrorDescription: "Failed to create client credential",
})
respondClientCredentialCreateError(c, logger, ownerUUID, err)
return
}

Expand All @@ -209,9 +184,40 @@ func (s *Server) CreateCurrentUserClientCredential(c *gin.Context) {
c.JSON(http.StatusCreated, apiResp)
}

// respondClientCredentialCreateError maps a credential create failure to a documented response.
// SEM@4f8575cbdd6a06b8391482229de6c8cb075359d3: map a client credential create error to its documented HTTP error response
func respondClientCredentialCreateError(c *gin.Context, logger *slogging.ContextLogger, ownerUUID uuid.UUID, err error) {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
logger.Debug("Request canceled during credential create: %v", err)
return
}
if errors.Is(err, dberrors.ErrDuplicate) || errors.Is(err, dberrors.ErrConstraint) {
logger.Warn("Client credential creation failed due to constraint: %v", err)
c.JSON(http.StatusBadRequest, Error{
Error: ErrorError(errcode.InvalidInput),
ErrorDescription: "The client credential could not be created with the supplied details",
})
return
}
if errors.Is(err, dberrors.ErrTransient) {
logger.Warn("Transient DB error creating client credential: %v", err)
c.Header("Retry-After", "30")
c.JSON(http.StatusServiceUnavailable, Error{
Error: ErrorError(errcode.ServiceUnavailable),
ErrorDescription: "Database temporarily unavailable, please retry",
})
return
}
logger.Error("Failed to create client credential for user %s: %v", ownerUUID, err)
c.JSON(http.StatusInternalServerError, Error{
Error: ErrorError(errcode.ServerError),
ErrorDescription: "Failed to create client credential",
})
}

// ListCurrentUserClientCredentials handles GET /me/client_credentials
// Retrieves all client credentials owned by the authenticated user (without secrets)
// SEM@32e22d40fa43dfa14fa39b14713e41c740ebe026: list the authenticated user's client credentials with pagination, omitting secrets (reads DB)
// SEM@db94db3c8118cffb5220bf9a44c2fde0ada20427: list the authenticated user's client credentials with pagination, omitting secrets (reads DB)
func (s *Server) ListCurrentUserClientCredentials(c *gin.Context, params ListCurrentUserClientCredentialsParams) {
logger := slogging.Get().WithContext(c)
userUUID := c.GetString("userInternalUUID")
Expand Down Expand Up @@ -317,7 +323,7 @@ func (s *Server) ListCurrentUserClientCredentials(c *gin.Context, params ListCur

// DeleteCurrentUserClientCredential handles DELETE /me/client_credentials/{credential_id}
// Permanently deletes a client credential
// SEM@9750a568b8ffb60cfd241d61263b7e23f990899e: delete and revoke a client credential owned by the authenticated user (writes DB)
// SEM@db94db3c8118cffb5220bf9a44c2fde0ada20427: delete and revoke a client credential owned by the authenticated user (writes DB)
func (s *Server) DeleteCurrentUserClientCredential(c *gin.Context, credentialId openapi_types.UUID) {
logger := slogging.Get().WithContext(c)
userUUID := c.GetString("userInternalUUID")
Expand Down
31 changes: 31 additions & 0 deletions api/client_credentials_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"testing"
"time"

"github.com/ericfitz/tmi/internal/dberrors"
"github.com/ericfitz/tmi/internal/slogging"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -754,3 +756,32 @@ func TestCreateCurrentUserClientCredential_AddonLink(t *testing.T) {
w, _ = post(`{"name":"tf-wh","direct_write":true,"addon_id":"` + knownAddon.String() + `"}`)
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
}

// #1105: POST /me/client_credentials documents no 409, so a store constraint or
// duplicate error must map to a documented code (400 invalid_input).
func TestRespondClientCredentialCreateError(t *testing.T) {
gin.SetMode(gin.TestMode)

cases := []struct {
name string
err error
wantStatus int
wantCode string
}{
{"Constraint", fmt.Errorf("insert: %w", dberrors.ErrConstraint), http.StatusBadRequest, "invalid_input"},
{"Duplicate", fmt.Errorf("insert: %w", dberrors.ErrDuplicate), http.StatusBadRequest, "invalid_input"},
{"Transient", fmt.Errorf("insert: %w", dberrors.ErrTransient), http.StatusServiceUnavailable, "service_unavailable"},
{"Unknown", fmt.Errorf("boom"), http.StatusInternalServerError, "server_error"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c, w := CreateTestGinContextWithBody("POST", "/me/client_credentials", "application/json", []byte(testCredBody))
respondClientCredentialCreateError(c, slogging.Get().WithContext(c), uuid.New(), tc.err)

assert.Equal(t, tc.wantStatus, w.Code)
var errResp Error
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &errResp))
assert.Equal(t, tc.wantCode, string(errResp.Error))
})
}
}
5 changes: 5 additions & 0 deletions api/config_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,11 @@ func TestUpdateSystemSetting_TimmyValidation(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid_input")
})
t.Run("negative max tokens is 400", func(t *testing.T) {
w := put(t, "dev", "timmy.llm_max_tokens", `{"value": "-5", "type": "int"}`)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid_input")
})
t.Run("development dump flag true is 200", func(t *testing.T) {
w := put(t, "dev", "timmy.dump_extracted_text_to_note", `{"value": "true", "type": "bool"}`)
assert.Equal(t, http.StatusOK, w.Code)
Expand Down
Loading
Loading