{"record":{"id":"3a39e5e86aa04054","repo":"JuliusBrussee/caveman","slug":"tenantkey-safe-organization-and-project-scope-are","errorCode":null,"errorMessage":"tenantkey: safe organization and project scope are required","messagePattern":"tenantkey: safe organization and project scope are required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/tenantkey/tenantkey.go","lineNumber":45,"sourceCode":"\t}\n\treturn prefix + \":\" + month, nil\n}\n\n// MonthlySpendEvent returns idempotency-marker key for one spend event.\nfunc MonthlySpendEvent(organizationID, projectID, month, requestID string) (string, error) {\n\tprefix, err := projectPrefix(\"spend-event\", organizationID, projectID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !safePart(month) || !safePart(requestID) {\n\t\treturn \"\", errors.New(\"tenantkey: safe month and request id are required\")\n\t}\n\treturn prefix + \":\" + month + \":\" + requestID, nil\n}\n\nfunc projectPrefix(namespace, organizationID, projectID string) (string, error) {\n\tif !safePart(organizationID) || !safePart(projectID) {\n\t\treturn \"\", errors.New(\"tenantkey: safe organization and project scope are required\")\n\t}\n\treturn \"cave:\" + namespace + \":\" + organizationID + \":\" + projectID, nil\n}\n\nfunc safePart(value string) bool {\n\tvalue = strings.TrimSpace(value)\n\treturn value != \"\" && !strings.ContainsAny(value, \":\\r\\n\\x00\")\n}\n","sourceCodeStart":27,"sourceCodeEnd":54,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/tenantkey/tenantkey.go#L27-L54","documentation":"tenantkey builds tenant-scoped cache keys as colon-delimited strings like 'cave:<namespace>:<orgID>:<projectID>'. It rejects organizationID or projectID values that are empty, whitespace-only, or contain ':', CR, LF, or NUL, because such values would collide with or forge other tenants' key namespaces. projectPrefix throws this error when safePart fails for either scope component passed by CavePlan, MonthlySpend, or MonthlySpendEvent.","triggerScenarios":"Calling tenantkey.CavePlan, tenantkey.MonthlySpend, or tenantkey.MonthlySpendEvent with an empty organizationID or projectID, a value that is only whitespace, or a value containing ':' (e.g. a raw UUID with colons or an unvalidated user-supplied identifier).","commonSituations":"Config or environment variables for org/project scope left unset so they arrive as empty strings; upstream APIs returning identifiers with colon separators; records where the org/project field was never populated; trimming failures where a whitespace-only ID passes naive emptiness checks elsewhere.","solutions":["Log the organizationID and projectID values at the call site and fix whatever supplies them (config loader, DB record, request parsing) so both are non-empty and free of ':' before calling tenantkey functions","Validate identifiers at your API boundary: reject values that are empty after TrimSpace or contain ':', '\\r', '\\n', or '\\x00'","If the ID legitimately contains ':' (e.g. a composed identifier), map it to a safe canonical form (UUID, numeric ID) before building the key"],"exampleFix":"// before\nkey, err := tenantkey.CavePlan(cfg.OrgID, cfg.ProjectID) // both may be \"\"\n// after\norg, proj := strings.TrimSpace(cfg.OrgID), strings.TrimSpace(cfg.ProjectID)\nif org == \"\" || proj == \"\" || strings.ContainsAny(org+proj, \":\\r\\n\\x00\") {\n    return fmt.Errorf(\"invalid tenant scope: org=%q project=%q\", org, proj)\n}\nkey, err := tenantkey.CavePlan(org, proj)","handlingStrategy":"validation","validationCode":"func validScope(s string) bool {\n\ts = strings.TrimSpace(s)\n\treturn s != \"\" && !strings.ContainsAny(s, \":\\r\\n\\x00\")\n}\nif !validScope(orgID) || !validScope(projectID) { return errors.New(\"invalid tenant scope\") }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate org/project IDs at the API/config boundary before they reach key-building code","Never compose cache keys from raw user input containing ':'","Trim identifiers centrally in your config loader so whitespace-only values fail early"],"tags":["go","validation","tenant-isolation","cache-keys"],"backgroundTag":"invalid-argument-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}