diff --git a/coderd/audit.go b/coderd/audit.go
index bf9334714446a..03392641681c2 100644
--- a/coderd/audit.go
+++ b/coderd/audit.go
@@ -244,13 +244,13 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
StatusCode: dblog.StatusCode,
AdditionalFields: dblog.AdditionalFields,
User: user,
- Description: auditLogDescription(dblog, additionalFields),
+ Description: auditLogDescription(dblog),
ResourceLink: resourceLink,
IsDeleted: isDeleted,
}
}
-func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields audit.AdditionalFields) string {
+func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
str := fmt.Sprintf("{user} %s",
codersdk.AuditAction(alog.Action).Friendly(),
)
@@ -261,19 +261,6 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow, additionalFields a
return str
}
- // Strings for starting/stopping workspace builds follow the below format:
- // "{user | 'Coder automatically'} started build #{build_number} for workspace {target}"
- // where target is a workspace (name) instead of a workspace build
- // passed in on the FE via AuditLog.AdditionalFields rather than derived in request.go:35
- if alog.ResourceType == database.ResourceTypeWorkspaceBuild && alog.Action != database.AuditActionDelete {
- if len(additionalFields.BuildNumber) == 0 {
- str += " build for"
- } else {
- str += fmt.Sprintf(" build #%s for",
- additionalFields.BuildNumber)
- }
- }
-
// We don't display the name (target) for git ssh keys. It's fairly long and doesn't
// make too much sense to display.
if alog.ResourceType == database.ResourceTypeGitSshKey {
diff --git a/site/package.json b/site/package.json
index 57e278858a1c5..2cc3c480f0b34 100644
--- a/site/package.json
+++ b/site/package.json
@@ -45,6 +45,7 @@
"@xstate/inspect": "0.6.5",
"@xstate/react": "3.0.1",
"axios": "0.26.1",
+ "canvas": "^2.11.0",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "3.0.0",
"color-convert": "2.0.1",
@@ -110,7 +111,6 @@
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.45.1",
"@xstate/cli": "0.3.0",
- "canvas": "2.10.0",
"chromatic": "6.15.0",
"eslint": "8.33.0",
"eslint-config-prettier": "8.5.0",
diff --git a/site/src/components/AuditLogRow/AuditLogDescription.tsx b/site/src/components/AuditLogRow/AuditLogDescription.tsx
deleted file mode 100644
index a17f88073f95a..0000000000000
--- a/site/src/components/AuditLogRow/AuditLogDescription.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { FC } from "react"
-import { AuditLog } from "api/typesGenerated"
-import { Link as RouterLink } from "react-router-dom"
-import Link from "@material-ui/core/Link"
-import { makeStyles } from "@material-ui/core/styles"
-import i18next from "i18next"
-
-export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
- auditLog,
-}): JSX.Element => {
- const classes = useStyles()
- const { t } = i18next
-
- let target = auditLog.resource_target.trim()
- let user = auditLog.user
- ? auditLog.user.username.trim()
- : t("auditLog:table.logRow.unknownUser")
-
- if (auditLog.resource_type === "workspace_build") {
- // audit logs with a resource_type of workspace build use workspace name as a target
- target = auditLog.additional_fields?.workspace_name?.trim()
- // workspaces can be started/stopped by a user, or kicked off automatically by Coder
- user =
- auditLog.additional_fields?.build_reason &&
- auditLog.additional_fields?.build_reason !== "initiator"
- ? t("auditLog:table.logRow.buildReason")
- : user
- }
-
- // SSH key entries have no links
- if (auditLog.resource_type === "git_ssh_key") {
- return (
-
- {auditLog.description
- .replace("{user}", `${auditLog.user?.username.trim()}`)
- .replace("{target}", `${target}`)}
-
- )
- }
-
- const truncatedDescription = auditLog.description
- .replace("{user}", `${user}`)
- .replace("{target}", "")
-
- return (
-
- {truncatedDescription}
- {auditLog.resource_link ? (
-
- {target}
-
- ) : (
- {target}
- )}
- {auditLog.is_deleted && (
-
- <>{t("auditLog:table.logRow.deletedLabel")}>
-
- )}
- {/* logs for workspaces created on behalf of other users indicate ownership in the description */}
- {auditLog.additional_fields.workspace_owner &&
- auditLog.additional_fields.workspace_owner !== "unknown" && (
-
- <>
- {t("auditLog:table.logRow.onBehalfOf", {
- owner: auditLog.additional_fields.workspace_owner,
- })}
- >
-
- )}
-
- )
-}
-
-const useStyles = makeStyles((theme) => ({
- deletedLabel: {
- ...theme.typography.caption,
- color: theme.palette.text.secondary,
- },
-}))
diff --git a/site/src/components/AuditLogRow/AuditLogDescription.test.tsx b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx
similarity index 58%
rename from site/src/components/AuditLogRow/AuditLogDescription.test.tsx
rename to site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx
index ad618c4b1b8d3..cb07125efaeba 100644
--- a/site/src/components/AuditLogRow/AuditLogDescription.test.tsx
+++ b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx
@@ -7,9 +7,13 @@ import {
MockAuditLogUnsuccessfulLoginUnknownUser,
} from "testHelpers/entities"
import { AuditLogDescription } from "./AuditLogDescription"
-import { AuditLogRow } from "./AuditLogRow"
-import { render } from "../../testHelpers/renderHelpers"
+import { AuditLogRow } from "../AuditLogRow"
+import { render } from "testHelpers/renderHelpers"
import { screen } from "@testing-library/react"
+import { i18n } from "i18n"
+
+const t = (str: string, variables?: Record) =>
+ i18n.t(str, variables)
const getByTextContent = (text: string) => {
return screen.getByText((_, element) => {
@@ -25,17 +29,14 @@ describe("AuditLogDescription", () => {
it("renders the correct string for a workspace create audit log", async () => {
render()
- expect(
- getByTextContent("TestUser created workspace bruno-dev"),
- ).toBeDefined()
+ expect(screen.getByText("TestUser created workspace")).toBeDefined()
+ expect(screen.getByText("bruno-dev")).toBeDefined()
})
it("renders the correct string for a workspace_build stop audit log", async () => {
render()
- expect(
- getByTextContent("TestUser stopped build for workspace test2"),
- ).toBeDefined()
+ expect(getByTextContent("TestUser stopped workspace test2")).toBeDefined()
})
it("renders the correct string for a workspace_build audit log with a duplicate word", async () => {
@@ -48,7 +49,7 @@ describe("AuditLogDescription", () => {
render()
expect(
- getByTextContent("TestUser stopped build for workspace workspace"),
+ getByTextContent("TestUser stopped workspace workspace"),
).toBeDefined()
})
it("renders the correct string for a workspace created for a different owner", async () => {
@@ -57,27 +58,68 @@ describe("AuditLogDescription", () => {
auditLog={MockWorkspaceCreateAuditLogForDifferentOwner}
/>,
)
+
expect(
- getByTextContent(
- `TestUser created workspace bruno-dev on behalf of ${MockWorkspaceCreateAuditLogForDifferentOwner.additional_fields.workspace_owner}`,
+ screen.getByText(
+ `on behalf of ${MockWorkspaceCreateAuditLogForDifferentOwner.additional_fields.workspace_owner}`,
+ { exact: false },
),
).toBeDefined()
})
it("renders the correct string for successful login", async () => {
render()
- expect(getByTextContent(`TestUser logged in`)).toBeDefined()
+
+ expect(
+ screen.getByText(
+ t("auditLog:table.logRow.description.unlinkedAuditDescription", {
+ truncatedDescription: `${MockAuditLogSuccessfulLogin.user?.username} logged in`,
+ target: "",
+ onBehalfOf: undefined,
+ })
+ .replace(/<[^>]*>/g, " ")
+ .replace(/\s{2,}/g, " ")
+ .trim(),
+ ),
+ ).toBeInTheDocument()
+
const statusPill = screen.getByRole("status")
expect(statusPill).toHaveTextContent("201")
})
it("renders the correct string for unsuccessful login for a known user", async () => {
render()
- expect(getByTextContent(`TestUser logged in`)).toBeDefined()
+
+ expect(
+ screen.getByText(
+ t("auditLog:table.logRow.description.unlinkedAuditDescription", {
+ truncatedDescription: `${MockAuditLogUnsuccessfulLoginKnownUser.user?.username} logged in`,
+ target: "",
+ onBehalfOf: undefined,
+ })
+ .replace(/<[^>]*>/g, " ")
+ .replace(/\s{2,}/g, " ")
+ .trim(),
+ ),
+ ).toBeInTheDocument()
+
const statusPill = screen.getByRole("status")
expect(statusPill).toHaveTextContent("401")
})
it("renders the correct string for unsuccessful login for an unknown user", async () => {
render()
- expect(getByTextContent(`an unknown user logged in`)).toBeDefined()
+
+ expect(
+ screen.getByText(
+ t("auditLog:table.logRow.description.unlinkedAuditDescription", {
+ truncatedDescription: "an unknown user logged in",
+ target: "",
+ onBehalfOf: undefined,
+ })
+ .replace(/<[^>]*>/g, " ")
+ .replace(/\s{2,}/g, " ")
+ .trim(),
+ ),
+ ).toBeInTheDocument()
+
const statusPill = screen.getByRole("status")
expect(statusPill).toHaveTextContent("401")
})
diff --git a/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx
new file mode 100644
index 0000000000000..129e48094165e
--- /dev/null
+++ b/site/src/components/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx
@@ -0,0 +1,68 @@
+import { FC } from "react"
+import { AuditLog } from "api/typesGenerated"
+import { Link as RouterLink } from "react-router-dom"
+import Link from "@material-ui/core/Link"
+import { Trans, useTranslation } from "react-i18next"
+import { BuildAuditDescription } from "./BuildAuditDescription"
+
+export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
+ auditLog,
+}): JSX.Element => {
+ const { t } = useTranslation("auditLog")
+
+ let target = auditLog.resource_target.trim()
+ const user = auditLog.user ? auditLog.user.username.trim() : "an unknown user"
+
+ if (auditLog.resource_type === "workspace_build") {
+ return
+ }
+
+ // SSH key entries have no links
+ if (auditLog.resource_type === "git_ssh_key") {
+ target = ""
+ }
+
+ const truncatedDescription = auditLog.description
+ .replace("{user}", `${user}`)
+ .replace("{target}", "")
+
+ // logs for workspaces created on behalf of other users indicate ownership in the description
+ const onBehalfOf =
+ auditLog.additional_fields.workspace_owner &&
+ auditLog.additional_fields.workspace_owner !== "unknown" &&
+ auditLog.additional_fields.workspace_owner !== auditLog.user?.username
+ ? `on behalf of ${auditLog.additional_fields.workspace_owner}`
+ : ""
+
+ if (auditLog.resource_link) {
+ return (
+
+
+ {"{{truncatedDescription}}"}
+
+ {"{{target}}"}
+
+ {"{{onBehalfOf}}"}
+
+
+ )
+ }
+
+ return (
+
+
+ {"{{truncatedDescription}}"}
+ {"{{target}}"}
+ {"{{onBehalfOf}}"}
+
+
+ )
+}
diff --git a/site/src/components/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx b/site/src/components/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx
new file mode 100644
index 0000000000000..0bce7f981ca71
--- /dev/null
+++ b/site/src/components/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx
@@ -0,0 +1,52 @@
+import { Trans, useTranslation } from "react-i18next"
+import { AuditLog } from "api/typesGenerated"
+import { FC } from "react"
+import { Link as RouterLink } from "react-router-dom"
+import Link from "@material-ui/core/Link"
+
+export const BuildAuditDescription: FC<{ auditLog: AuditLog }> = ({
+ auditLog,
+}): JSX.Element => {
+ const { t } = useTranslation("auditLog")
+
+ const workspaceName = auditLog.additional_fields?.workspace_name?.trim()
+ // workspaces can be started/stopped by a user, or kicked off automatically by Coder
+ const user =
+ auditLog.additional_fields?.build_reason &&
+ auditLog.additional_fields?.build_reason !== "initiator"
+ ? "Coder automatically"
+ : auditLog.user?.username.trim()
+
+ const action = auditLog.action === "start" ? "started" : "stopped"
+
+ if (auditLog.resource_link) {
+ return (
+
+
+ {"{{user}}"}
+
+ {"{{action}}"}
+
+ workspace{"{{workspaceName}}"}
+
+
+ )
+ }
+
+ return (
+
+
+ {"{{user}}"}
+ {"{{action}}"}workspace{"{{workspaceName}}"}
+
+
+ )
+}
diff --git a/site/src/components/AuditLogRow/AuditLogDescription/index.ts b/site/src/components/AuditLogRow/AuditLogDescription/index.ts
new file mode 100644
index 0000000000000..590ff7c7b0e17
--- /dev/null
+++ b/site/src/components/AuditLogRow/AuditLogDescription/index.ts
@@ -0,0 +1 @@
+export { AuditLogDescription } from "./AuditLogDescription"
diff --git a/site/src/components/AuditLogRow/AuditLogDiff.tsx b/site/src/components/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx
similarity index 100%
rename from site/src/components/AuditLogRow/AuditLogDiff.tsx
rename to site/src/components/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx
diff --git a/site/src/components/AuditLogRow/auditUtils.test.ts b/site/src/components/AuditLogRow/AuditLogDiff/auditUtils.test.ts
similarity index 100%
rename from site/src/components/AuditLogRow/auditUtils.test.ts
rename to site/src/components/AuditLogRow/AuditLogDiff/auditUtils.test.ts
diff --git a/site/src/components/AuditLogRow/auditUtils.ts b/site/src/components/AuditLogRow/AuditLogDiff/auditUtils.ts
similarity index 100%
rename from site/src/components/AuditLogRow/auditUtils.ts
rename to site/src/components/AuditLogRow/AuditLogDiff/auditUtils.ts
diff --git a/site/src/components/AuditLogRow/AuditLogDiff/index.ts b/site/src/components/AuditLogRow/AuditLogDiff/index.ts
new file mode 100644
index 0000000000000..7243291182ca9
--- /dev/null
+++ b/site/src/components/AuditLogRow/AuditLogDiff/index.ts
@@ -0,0 +1,2 @@
+export { AuditLogDiff } from "./AuditLogDiff"
+export { determineGroupDiff } from "./auditUtils"
diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx
index 4813932a17351..c3459300be724 100644
--- a/site/src/components/AuditLogRow/AuditLogRow.tsx
+++ b/site/src/components/AuditLogRow/AuditLogRow.tsx
@@ -13,10 +13,9 @@ import { UserAvatar } from "components/UserAvatar/UserAvatar"
import { useState } from "react"
import { PaletteIndex } from "theme/palettes"
import userAgentParser from "ua-parser-js"
-import { AuditLogDiff } from "./AuditLogDiff"
-import i18next from "i18next"
+import { AuditLogDiff, determineGroupDiff } from "./AuditLogDiff"
+import { useTranslation } from "react-i18next"
import { AuditLogDescription } from "./AuditLogDescription"
-import { determineGroupDiff } from "./auditUtils"
const httpStatusColor = (httpStatus: number): PaletteIndex => {
if (httpStatus >= 300 && httpStatus < 500) {
@@ -41,14 +40,11 @@ export const AuditLogRow: React.FC = ({
defaultIsDiffOpen = false,
}) => {
const styles = useStyles()
- const { t } = i18next
+ const { t } = useTranslation("auditLog")
const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen)
const diffs = Object.entries(auditLog.diff)
const shouldDisplayDiff = diffs.length > 0
const { os, browser } = userAgentParser(auditLog.user_agent)
- const displayBrowserInfo = browser.name
- ? `${browser.name} ${browser.version}`
- : t("auditLog:table.logRow.notAvailable")
let auditDiff = auditLog.diff
@@ -110,6 +106,11 @@ export const AuditLogRow: React.FC = ({
spacing={1}
>
+ {auditLog.is_deleted && (
+
+ <>{t("table.logRow.deletedLabel")}>
+
+ )}
{new Date(auditLog.time).toLocaleTimeString()}
@@ -119,25 +120,24 @@ export const AuditLogRow: React.FC = ({
{auditLog.ip && (
- <>{t("auditLog:table.logRow.ip")}>
+ <>{t("table.logRow.ip")}>
{auditLog.ip}
)}
-
-
- <>{t("auditLog:table.logRow.os")}>
-
- {os.name
- ? os.name
- : // https://github.com/i18next/next-i18next/issues/1795
- t("auditLog:table.logRow.notAvailable")}
-
-
-
-
- <>{t("auditLog:table.logRow.browser")}>
- {displayBrowserInfo}
-
+ {os.name && (
+
+ <>{t("table.logRow.os")}>
+ {os.name}
+
+ )}
+ {browser.name && (
+
+ <>{t("table.logRow.browser")}>
+
+ {browser.name} {browser.version}
+
+
+ )}
({
paddingRight: 10,
fontWeight: 600,
},
+
+ deletedLabel: {
+ ...theme.typography.caption,
+ color: theme.palette.text.secondary,
+ },
}))
diff --git a/site/src/i18n/en/auditLog.json b/site/src/i18n/en/auditLog.json
index 2a712f34b1c72..4ae30cf553e64 100644
--- a/site/src/i18n/en/auditLog.json
+++ b/site/src/i18n/en/auditLog.json
@@ -8,13 +8,16 @@
"emptyPage": "No audit logs available on this page",
"noLogs": "No audit logs available",
"logRow": {
+ "description": {
+ "linkedWorkspaceBuild": "{{user}} <1>{{action}}1> workspace {{workspaceName}}",
+ "unlinkedWorkspaceBuild": "{{user}} {{action}} workspace {{workspaceName}}",
+ "linkedAuditDescription": "{{truncatedDescription}} <1>{{target}}1> {{onBehalfOf}}",
+ "unlinkedAuditDescription": "{{truncatedDescription}} {{target}} {{onBehalfOf}}"
+ },
"deletedLabel": " (deleted)",
"ip": "IP: ",
"os": "OS: ",
- "browser": "Browser: ",
- "onBehalfOf": " on behalf of {{owner}}",
- "buildReason": "Coder automatically",
- "unknownUser": "an unknown user"
+ "browser": "Browser: "
}
},
"paywall": {
diff --git a/site/yarn.lock b/site/yarn.lock
index a504523c743af..2ea632bc7bd3b 100644
--- a/site/yarn.lock
+++ b/site/yarn.lock
@@ -5170,13 +5170,13 @@ caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001304, caniuse-lite@^1.0.300014
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795"
integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==
-canvas@2.10.0:
- version "2.10.0"
- resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.10.0.tgz#5f48c8d1ff86c96356809097020336c3a1ccce27"
- integrity sha512-A0RPxLcH0pPKAY2VN243LdCNcOJXAT8n7nJnN7TZMGv9OuF8we9wfpWgVT/eFMzi+cDYf/384w4BpfjGCD9aKQ==
+canvas@^2.11.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.0.tgz#7f0c3e9ae94cf469269b5d3a7963a7f3a9936434"
+ integrity sha512-bdTjFexjKJEwtIo0oRx8eD4G2yWoUOXP9lj279jmQ2zMnTQhT8C3512OKz3s+ZOaQlLbE7TuVvRDYDB3Llyy5g==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
- nan "^2.15.0"
+ nan "^2.17.0"
simple-get "^3.0.3"
capture-exit@^2.0.0:
@@ -10788,6 +10788,11 @@ minipass@^3.0.0, minipass@^3.1.1:
dependencies:
yallist "^4.0.0"
+minipass@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.2.tgz#26fc3364d5ea6cb971c6e5259eac67a0887510d1"
+ integrity sha512-4Hbzei7ZyBp+1aw0874YWpKOubZd/jc53/XU+gkYry1QV+VvrbO8icLM5CUtm4F0hyXn85DXYKEMIS26gitD3A==
+
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
@@ -10917,7 +10922,7 @@ mute-stream@0.0.8:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
-nan@^2.12.1, nan@^2.15.0:
+nan@^2.12.1, nan@^2.17.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
@@ -13689,7 +13694,7 @@ tar-stream@^2.0.1:
inherits "^2.0.3"
readable-stream "^3.1.1"
-tar@^6.0.2, tar@^6.1.11:
+tar@^6.0.2:
version "6.1.12"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6"
integrity sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==
@@ -13701,6 +13706,18 @@ tar@^6.0.2, tar@^6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
+tar@^6.1.11:
+ version "6.1.13"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b"
+ integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==
+ dependencies:
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ minipass "^4.0.0"
+ minizlib "^2.1.1"
+ mkdirp "^1.0.3"
+ yallist "^4.0.0"
+
telejson@^6.0.8:
version "6.0.8"
resolved "https://registry.yarnpkg.com/telejson/-/telejson-6.0.8.tgz#1c432db7e7a9212c1fbd941c3e5174ec385148f7"