Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
33 changes: 19 additions & 14 deletions actions/setup/js/ai_credits_context.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,24 @@ function parseMaxAICreditsExceededFromAuditLog(auditJsonlPathOverride) {
);
}

/**
* @param {unknown} entry
* @returns {boolean}
*/
function parseUnknownModelAICreditsFromAuditEntry(entry) {
if (!entry || typeof entry !== "object") return false;
const stack = [entry];
while (stack.length > 0) {
const node = stack.pop();
if (!node || typeof node !== "object") continue;
for (const [, value] of Object.entries(node)) {
if (value === UNKNOWN_MODEL_AI_CREDITS_TYPE) return true;
if (value && typeof value === "object") stack.push(value);
}
}
return false;
}

/**
* Detects an `unknown_model_ai_credits` error from the firewall audit log.
* This HTTP 400 error is emitted by the AWF API proxy when `maxAiCredits` is active and
Expand All @@ -268,20 +286,7 @@ function parseUnknownModelAICreditsFromAuditLog(auditJsonlPathOverride) {
auditJsonlPathOverride,
false,
content => content.includes(UNKNOWN_MODEL_AI_CREDITS_TYPE),
(acc, entry) => {
if (acc) return true;
if (!entry || typeof entry !== "object") return false;
const stack = [entry];
while (stack.length > 0) {
const node = stack.pop();
if (!node || typeof node !== "object") continue;
for (const [, value] of Object.entries(node)) {
if (value === UNKNOWN_MODEL_AI_CREDITS_TYPE) return true;
if (value && typeof value === "object") stack.push(value);
}
}
return false;
}
(acc, entry) => acc || parseUnknownModelAICreditsFromAuditEntry(entry)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/zoom-out] The acc || parseUnknownModelAICreditsFromAuditEntry(entry) form matches the pattern at line 253 (parseMaxAICreditsExceededFromAuditLog's reducer) and is the right call: it short-circuits the entry-parse when acc is already true. By contrast, line 189 (parseMaxAICreditsFromAuditLog) still uses the entry-first form fn(entry) || acc, which evaluates fn even after the accumulator has been set. This PR inadvertently exposes that inconsistency — a follow-up to align line 189 would complete the pattern unification.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change here. I kept the ai_credits_context.cjs extraction as-is and left the line 189 reducer ordering for a separate follow-up so this PR stays scoped to the review feedback on allowed_issue_fields.cjs.

);
}

Expand Down
15 changes: 2 additions & 13 deletions actions/setup/js/allowed_issue_fields.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,9 @@ function validateAllowedIssueFieldName(fieldName, allowedFields) {
* @returns {void}
*/
function validateAllowedIssueFields(issueFields, allowedFields) {
if (!Array.isArray(issueFields) || issueFields.length === 0) {
return;
}
if (!Array.isArray(allowedFields) || allowedFields.length === 0) {
return;
}
const allowedFieldSet = new Set(allowedFields.map(f => f.toLowerCase()));
if (allowedFieldSet.has("*")) {
return;
}
if (!Array.isArray(issueFields) || issueFields.length === 0) return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavioral change: falsy field.name now silently passes validation — the refactor unintentionally weakens the field-name guard, contradicting the PR claim that "behavior is unchanged."

💡 Details

The original validateAllowedIssueFields checked each field name directly against the allowedFieldSet:

// old
if (!allowedFieldSet.has(field.name.toLowerCase())) {
  throw new Error(...);
}

For field.name === "" this evaluates "".toLowerCase() === "", which is not in the set → throws a validation error.

The new delegate path starts with:

// inside validateAllowedIssueFieldName
if (!fieldName) return; // empty string is falsy → silently passes

So a field with name: "" now passes validation with no error instead of being rejected. While the JSDoc type says name: string, the runtime has no such enforcement guarantee. Any caller passing { name: "", value: ... } will see silently accepted invalid input instead of the previous validation error.

Suggested fix — add an explicit guard before delegating, or document that empty-named fields are now intentionally permitted:

function validateAllowedIssueFields(issueFields, allowedFields) {
  if (!Array.isArray(issueFields) || issueFields.length === 0) return;
  for (const field of issueFields) {
    validateAllowedIssueFieldName(field.name, allowedFields);
  }
}

If the callers are known to always produce non-empty field.name, update the JSDoc @param to @param {Array<{name: NonEmptyString, ...}>} or add an assertion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ee75179. validateAllowedIssueFields now rejects an empty field.name again, and I added a regression test covering that case.

for (const field of issueFields) {
if (!allowedFieldSet.has(field.name.toLowerCase())) {
throw new Error(`${ERR_VALIDATION}: issue field "${field.name}" is not in the allowed-fields list: ${allowedFields.join(", ")}`);
}
validateAllowedIssueFieldName(field.name, allowedFields);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] The refactor moves allowedFieldSet construction (and the empty/wildcard guards) from a single pre-loop site into each validateAllowedIssueFieldName call — O(N) Set builds instead of O(1). For current usage (small issueFields arrays) this is negligible, but good to be aware of if this path ever becomes hot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ee75179. The bulk validator now restores single-pass set construction, so this path no longer rebuilds the set for each field.

}
Comment thread
pelikhan marked this conversation as resolved.
}

Expand Down
Loading