v0.5.101: circular dependency mitigation, confluence enhancements, google tasks and bigquery integrations, workflow lock#3349
v0.5.101: circular dependency mitigation, confluence enhancements, google tasks and bigquery integrations, workflow lock#3349waleedlatif1 wants to merge 16 commits intomainfrom
Conversation
…3332) * fix(call-chain): x-sim-via propagation for API blocks and MCP tools * addres bugbot comment
* feat(google-sheets): add filter support to read operation * ran lint
* feat(google-translate): add Google Translate integration * fix(google-translate): api key as query param, fix docsLink, rename tool file
#3338) * feat(google): add missing tools for Gmail, Drive, Sheets, and Calendar * fix(google-drive): remove dead transformResponse from move tool
* feat(confluence): return page content in get page version tool * lint
* feat(api): audit log read endpoints for admin and enterprise * fix(api): address PR review — boolean coercion, cursor validation, detail scope * ran lint
* feat(workflow): lock/unlock workflow from context menu and panel * lint * fix(workflow): prevent duplicate lock notifications, no-op guard, fix orphaned JSDoc * improvement(workflow): memoize hasLockedBlocks to avoid inline recomputation * feat(google-translate): add Google Translate integration (#3337) * feat(google-translate): add Google Translate integration * fix(google-translate): api key as query param, fix docsLink, rename tool file * feat(google): add missing tools for Gmail, Drive, Sheets, and Calendar (#3338) * feat(google): add missing tools for Gmail, Drive, Sheets, and Calendar * fix(google-drive): remove dead transformResponse from move tool * feat(confluence): return page content in get page version tool (#3344) * feat(confluence): return page content in get page version tool * lint * feat(api): audit log read endpoints for admin and enterprise (#3343) * feat(api): audit log read endpoints for admin and enterprise * fix(api): address PR review — boolean coercion, cursor validation, detail scope * ran lint * unified list of languages for google translate * fix(workflow): respect snapshot view for panel lock toggle, remove unused disableAdmin prop * improvement(canvas-menu): remove lock icon from workflow lock toggle * feat(audit): record audit log for workflow lock/unlock
* feat(confluence): add get user by account ID tool * feat(confluence): add missing tools for tasks, blog posts, spaces, descendants, permissions, and properties Add 16 new Confluence operations: list/get/update tasks, update/delete blog posts, create/update/delete spaces, get page descendants, list space permissions, list/create/delete space properties. Includes API routes, tool definitions, block config wiring, OAuth scopes, and generated docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(confluence): add missing OAuth scopes to auth.ts provider config The OAuth authorization flow uses scopes from auth.ts, not oauth.ts. The 9 new scopes were only added to oauth.ts and the block config but not to the actual provider config in auth.ts, causing re-auth to still return tokens without the new scopes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * lint * fix(confluence): fix truncated get_user tool description in docs Remove apostrophe from description that caused MDX generation to truncate at the escape character. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(confluence): address PR review feedback - Move get_user from GET to POST to avoid exposing access token in URL - Add 400 validation for missing params in space-properties create/delete - Add null check for blog post version before update to prevent TypeError Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(confluence): add missing response fields for descendants and tasks - Add type and depth fields to page descendants (from Confluence API) - Add body field (storage format) to task list/get/update responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * lint * fix(confluence): use validatePathSegment for Atlassian account IDs validateAlphanumericId rejects valid Atlassian account IDs that contain colons (e.g. 557058:6b9c9931-4693-49c1-8b3a-931f1af98134). Use validatePathSegment with a custom pattern allowing colons instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ran lint * update mock * upgrade turborepo * fix(confluence): reject empty update body for space PUT Return 400 when neither name nor description is provided for space update, instead of sending an empty body to the Confluence API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(confluence): remove spaceId requirement for create_space and fix list_tasks pagination - Remove create_space from spaceId condition array since creating a space doesn't require a space ID input - Remove list_tasks from generic supportsCursor array so it uses its dedicated handler that correctly passes assignedTo and status filters during pagination Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ran lint * fixed type errors --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…s for loop support (#3346) * fix(terminal): thread executionOrder through child workflow SSE events for loop support * ran lint * fix(terminal): render iteration children through EntryNodeRow for workflow block expansion IterationNodeRow was rendering all children as flat BlockRow components, ignoring nodeType. Workflow blocks inside loop iterations were never rendered as WorkflowNodeRow, so they had no expand chevron or child tree. * fix(terminal): add childWorkflowBlockId to matchesEntryForUpdate Sub-executors reset executionOrderCounter, so child blocks across loop iterations share the same blockId + executionOrder. Without checking childWorkflowBlockId, updateConsole for iteration N overwrites entries from iterations 0..N-1, causing all child blocks to be grouped under the last iteration's workflow instance.
* feat(bigquery): add Google BigQuery integration * fix(bigquery): add auth provider, fix docsLink and insertedRows count * fix(bigquery): set pageToken visibility to user-or-llm for pagination * fix(bigquery): use prefixed export names to avoid aliased imports * lint * improvement(bigquery): destructure tool outputs with structured array/object types * lint
* feat(google-tasks): add Google Tasks integration * fix(google-tasks): return actual taskId in delete response * fix(google-tasks): use absolute imports and fix registry order * fix(google-tasks): rename list-task-lists to list_task_lists for doc generator * improvement(google-tasks): destructure task and taskList outputs with typed schemas * ran lint * improvement(google-tasks): add wandConfig for due date timestamp generation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| const response = await fetch(currentUrl, { | ||
| method: 'PUT', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: JSON.stringify(updateBody), | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
General fix approach: Ensure that user-controlled values used in URL construction cannot change the host, scheme, or unintended parts of the path, and cannot smuggle URL syntax. This can be done by (a) validating the parameter against a strict pattern and/or (b) encoding it safely as a path segment (e.g., encodeURIComponent). Because the host is already hard‑coded here, we primarily need to make sure blogPostId is strictly constrained and clearly treated as a safe path segment.
Best way to fix without changing functionality: Explicitly restrict blogPostId to a safe pattern (e.g., digits only, or a specific alphanumeric regex) via zod schema for the request body, so that the value is validated before it reaches the code that constructs currentUrl. That makes it clear to both humans and tools that blogPostId cannot contain /, ?, #, or other URL‑control characters. We can then optionally still apply encodeURIComponent when interpolating into the URL as defense in depth. The existing code is already using zod schemas (getBlogPostSchema, createBlogPostSchema); we should use createBlogPostSchema to parse and validate the request JSON at the beginning of the handler instead of destructuring directly from body. Within the schema, we can define blogPostId as a z.string().regex(...) that only allows safe characters, and retain the current validateAlphanumericId check for backward compatibility or remove redundancy later. After parsing with the schema, we use the parsed blogPostId (optionally URL-encoded) in currentUrl. This is a minimal, local change inside apps/sim/app/api/tools/confluence/blogposts/route.ts.
Concretely:
- In the handler around lines 291–305, replace
const body = await request.json()and the subsequent manual presence checks withconst parsed = createBlogPostSchema.safeParse(await request.json()), returning a 400 if parsing fails. Then destructure fromparsed.data. - When constructing
currentUrlat line 320, wrapblogPostIdinencodeURIComponent(blogPostId)to ensure it is treated as a literal path segment, even if future changes loosen validation. - No new imports are required;
encodeURIComponentis globally available in Node/Next runtimes.
| @@ -294,16 +294,16 @@ | ||
| return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) | ||
| } | ||
|
|
||
| const body = await request.json() | ||
| const { domain, accessToken, blogPostId, title, content, cloudId: providedCloudId } = body | ||
|
|
||
| if (!domain || !accessToken || !blogPostId) { | ||
| const parsedBody = createBlogPostSchema.safeParse(await request.json()) | ||
| if (!parsedBody.success) { | ||
| return NextResponse.json( | ||
| { error: 'Domain, access token, and blog post ID are required' }, | ||
| { error: parsedBody.error.flatten().fieldErrors || 'Invalid request body' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const { domain, accessToken, blogPostId, title, content, cloudId: providedCloudId } = parsedBody.data | ||
|
|
||
| const blogPostIdValidation = validateAlphanumericId(blogPostId, 'blogPostId', 255) | ||
| if (!blogPostIdValidation.isValid) { | ||
| return NextResponse.json({ error: blogPostIdValidation.error }, { status: 400 }) | ||
| @@ -317,7 +312,9 @@ | ||
| } | ||
|
|
||
| // Fetch current blog post to get version number | ||
| const currentUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}` | ||
| const currentUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${encodeURIComponent( | ||
| blogPostId | ||
| )}` | ||
| const currentResponse = await fetch(currentUrl, { | ||
| headers: { | ||
| Accept: 'application/json', |
| const response = await fetch(url, { | ||
| method: 'GET', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
General approach: ensure that all user-influenced values interpolated into the request URL path (cloudId, pageId) are strictly validated against a tight, local allow-list of characters/format before being used. This means rejecting any value that contains /, ?, #, \, or other characters that could break out of the intended path segment, and constraining IDs to the formats Confluence expects, rather than just “alphanumeric up to length N”.
Best concrete fix here: in apps/sim/app/api/tools/confluence/page-descendants/route.ts, add small helper functions (or inline checks) to enforce a strict pattern for pageId and to harden cloudId usage. For pageId, Confluence Cloud page IDs are typically numeric; if we assume that, we can enforce /^\d+$/. If we don’t want to change semantics too much, we can at least require a restricted set like /^[A-Za-z0-9_-]+$/ and disallow any path separators or URL control characters. For cloudId, even though validateJiraCloudId is called, we can add a cheap local guard ensuring it only contains hex digits and dashes (the usual Atlassian cloud ID format) and no slashes or other reserved characters. These local checks are added in this route only, so they don’t change global behavior and are easy to reason about.
Concretely:
- Leave imports as-is; no new dependencies are required.
- After parsing the body and before constructing the URL:
- Replace the current
pageIdValidation = validateAlphanumericId(...)block with a stricter local check using a regular expression, returning a 400 if it fails. - Keep the
validateJiraCloudIdcall, but additionally enforce a strict regex forcloudId(e.g.,/^[0-9a-fA-F-]+$/) before using it in the URL.
- Replace the current
- Do not change the structure of the fetch call or its headers; only ensure the values going into the URL are tightly sanitized.
These changes maintain the existing functionality for valid inputs (Confluence-like IDs) while preventing any malformed or malicious IDs from influencing the request URL path.
| @@ -38,9 +38,15 @@ | ||
| return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) | ||
| } | ||
|
|
||
| const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) | ||
| if (!pageIdValidation.isValid) { | ||
| return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) | ||
| // Enforce a strict, URL-safe pageId format to prevent SSRF via path manipulation. | ||
| // Confluence page IDs are numeric; if that ever changes, this can be relaxed to a | ||
| // more general but still URL-safe pattern like /^[A-Za-z0-9_-]+$/. | ||
| const pageIdPattern = /^[0-9]+$/ | ||
| if (!pageIdPattern.test(pageId)) { | ||
| return NextResponse.json( | ||
| { error: 'Invalid pageId format; expected a numeric Confluence page ID.' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) | ||
| @@ -50,6 +56,15 @@ | ||
| return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) | ||
| } | ||
|
|
||
| // Additional hardening: ensure cloudId cannot break out of its URL path segment. | ||
| const cloudIdPattern = /^[0-9a-fA-F-]+$/ | ||
| if (!cloudIdPattern.test(cloudId)) { | ||
| return NextResponse.json( | ||
| { error: 'Invalid cloudId format; expected a hexadecimal Atlassian cloud ID.' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const queryParams = new URLSearchParams() | ||
| queryParams.append('limit', String(Math.min(limit, 250))) | ||
|
|
| fetch(versionUrl, { | ||
| method: 'GET', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }), |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Copilot Autofix
AI about 7 hours ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| const getResponse = await fetch(getUrl, { | ||
| method: 'GET', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Copilot Autofix
AI about 7 hours ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| const response = await fetch(url, { | ||
| method: 'PUT', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: JSON.stringify(updateBody), | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
In general, SSRF mitigation requires ensuring that user-controlled data cannot affect the destination host/scheme and can only influence tightly-constrained parts of the path or query, with strong validation. This code already fixes the scheme and host; the remaining concern is ensuring taskId cannot be used to alter the path structure or inject additional URL components. The best fix here is to (1) enforce a strict pattern on taskId locally, independent of external helpers, and/or (2) treat taskId as a path segment and reject or sanitize any value that is not a single safe segment. This preserves existing functionality while eliminating the possibility that a malicious taskId could lead to unintended endpoints.
Concretely, in apps/sim/app/api/tools/confluence/tasks/route.ts, inside the if (action === 'update' && taskId) block, right after the existing validateAlphanumericId check, add an additional explicit validation step that constrains taskId to a safe pattern, e.g. /^[A-Za-z0-9_-]+$/. If the value fails this check, return a 400 error. Then, when constructing URLs, use this validated taskId (and the validated cloudId) directly in the template strings. This does not change observable behavior for legitimate callers whose taskId already conforms, but it guarantees that no path traversal or special characters can be introduced via taskId. No new imports are strictly necessary; we can use a local regular expression.
| @@ -61,6 +61,15 @@ | ||
| return NextResponse.json({ error: taskIdValidation.error }, { status: 400 }) | ||
| } | ||
|
|
||
| // Extra hardening: ensure taskId is a single safe URL path segment | ||
| const safeTaskIdPattern = /^[A-Za-z0-9_-]+$/ | ||
| if (!safeTaskIdPattern.test(taskId)) { | ||
| return NextResponse.json( | ||
| { error: 'Invalid taskId format' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| // First fetch the current task to get required fields | ||
| const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` | ||
| const getResponse = await fetch(getUrl, { |
| const response = await fetch(url, { | ||
| method: 'GET', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
General approach: Ensure that any user-controlled portion of a URL cannot affect the scheme, host, or path structure. For path segments, enforce a strict character allowlist and/or encode the value so special characters cannot alter the URL.
Best fix here: Harden the taskId usage by (1) explicitly constraining it to safe characters and (2) ensuring it is treated as a literal path segment, not raw URL syntax. We already have validateAlphanumericId, but CodeQL does not understand that. We can add a small, explicit validation/normalization step right before building the URL that guarantees the taskId is a safe path segment, using a simple regex. This does not change behavior for valid IDs (which are already alphanumeric) but makes the safety condition self-contained and obvious.
Concretely in apps/sim/app/api/tools/confluence/tasks/route.ts around lines 134–141:
- After the existing
validateAlphanumericIdcheck passes, add an explicit check enforcing a strict pattern such as/^[A-Za-z0-9_-]+$/. If it fails, return a 400 error. - Optionally (and conservatively), keep the URL templating but rely on this restrictor so no reserved URL characters can ever be present. Using
encodeURIComponent(taskId)is also safe, but iftaskIdmust remain unchanged visually in logs or downstream API expectations, the explicit regex is clearer and preserves the string as-is for valid values.
Changes needed:
- No new imports; we only use built-in regex and existing Next.js/TS features.
- Only modify the
if (taskId) { ... }block whereurlis constructed, inserting a short validation block just beforeconst url = ....
| @@ -137,6 +137,15 @@ | ||
| return NextResponse.json({ error: taskIdValidation.error }, { status: 400 }) | ||
| } | ||
|
|
||
| // Ensure taskId is safe to use as a URL path segment and cannot alter the request target | ||
| const safeTaskIdPattern = /^[A-Za-z0-9_-]+$/ | ||
| if (!safeTaskIdPattern.test(taskId)) { | ||
| return NextResponse.json( | ||
| { error: 'Invalid taskId format' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` | ||
|
|
||
| logger.info(`Fetching task ${taskId}`) |
| const response = await fetch(url, { | ||
| method: 'GET', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
In general, SSRF must be mitigated by ensuring that user input cannot change the destination host or introduce dangerous path components, and by validating or mapping any user-supplied identifiers used in URLs against a strict allow‑list or format. Here, cloudId can be supplied by the client via providedCloudId, and even though we already call validateJiraCloudId, CodeQL still considers it tainted. The best targeted fix is to add a strict, local validation/sanitization step for cloudId in this route, ensuring that only an expected safe format is accepted before constructing the URL.
Concretely, in apps/sim/app/api/tools/confluence/tasks/route.ts, immediately after computing cloudId and running validateJiraCloudId, we can add an extra check that cloudId matches a conservative pattern (for example, a UUID-like pattern or at least alphanumeric with dashes) and reject the request with HTTP 400 if it does not. This guarantees there are no /, ?, #, or similar special characters that could alter the path or query structure of the URL. We do not need any new imports: we can use a local RegExp. This keeps functionality the same for valid cloudId values while making the data demonstrably safe for use in the URL and silencing the CodeQL SSRF warning.
| @@ -54,6 +54,16 @@ | ||
| return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) | ||
| } | ||
|
|
||
| // Extra hardening: ensure cloudId is safe to embed in a URL path segment | ||
| // Allow only UUID-like format: hex digits and dashes | ||
| const CLOUD_ID_SAFE_PATTERN = /^[a-fA-F0-9-]+$/ | ||
| if (!CLOUD_ID_SAFE_PATTERN.test(cloudId)) { | ||
| return NextResponse.json( | ||
| { error: 'Invalid cloudId format' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| // Update a task | ||
| if (action === 'update' && taskId) { | ||
| const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255) |
| const response = await fetch(url, { | ||
| method: 'GET', | ||
| headers: { | ||
| Accept: 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 7 hours ago
In general, SSRF issues are fixed by ensuring that untrusted input cannot directly control the destination of outgoing HTTP requests, particularly the hostname and significant parts of the path. For path segments derived from user input, either (1) strictly validate them against an allow-list or strong format that cannot be abused for path traversal, or (2) avoid trusting client-supplied identifiers by resolving them server-side based on trusted data.
For this specific code, the safest and simplest fix that preserves functionality is to stop allowing the client to provide cloudId at all. Instead, always derive cloudId using the trusted helper getConfluenceCloudId(domain, accessToken), which presumably talks to Atlassian and returns the correct ID for the given domain. This removes the tainted providedCloudId from the data flow entirely. Concretely:
- In
apps/sim/app/api/tools/confluence/user/route.ts, change line 23 so that you only destructuredomain,accessToken, andaccountIdfrombody, removingcloudId: providedCloudId. - Replace line 47 so that
cloudIdis always obtained fromgetConfluenceCloudId(domain, accessToken)and is not conditionally overridden by a user-provided value. - Keep the existing
validateJiraCloudIdcheck so that whatevergetConfluenceCloudIdreturns is still validated before being used in the URL.
No new imports or helper methods are required; we are just simplifying the assignment of cloudId inside the existing file.
| @@ -20,7 +20,7 @@ | ||
| } | ||
|
|
||
| const body = await request.json() | ||
| const { domain, accessToken, accountId, cloudId: providedCloudId } = body | ||
| const { domain, accessToken, accountId } = body | ||
|
|
||
| if (!domain) { | ||
| return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) | ||
| @@ -44,7 +44,7 @@ | ||
| return NextResponse.json({ error: accountIdValidation.error }, { status: 400 }) | ||
| } | ||
|
|
||
| const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) | ||
| const cloudId = await getConfluenceCloudId(domain, accessToken) | ||
|
|
||
| const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') | ||
| if (!cloudIdValidation.isValid) { |
* feat(sidebar): add lock/unlock to workflow registry context menu * docs(tools): add manual descriptions to google_books and table * docs(tools): add manual descriptions to google_bigquery and google_tasks * fix(sidebar): avoid unnecessary store subscriptions and fix mixed lock state toggle * fix(sidebar): use getWorkflowLockToggleIds utility for lock toggle Replaces manual pivot-sorting logic with the existing utility function, which handles block ordering and no-op guards consistently. * lint
Uh oh!
There was an error while loading. Please reload this page.