From 60f6061d496914b6898061f20601227b1bb88e8b Mon Sep 17 00:00:00 2001 From: Darren Clarke Date: Wed, 29 Mar 2023 14:43:27 +0200 Subject: [PATCH] Add more graphql support to Link --- apps/link/components/ArticleCreateDialog.tsx | 56 ++++------------ apps/link/components/TicketDetail.tsx | 1 + apps/link/components/TicketEdit.tsx | 57 ++++++++++------ apps/link/components/ZammadWrapper.tsx | 25 ++++++- apps/link/graphql/getTicketQuery.ts | 40 ++++++++++++ apps/link/graphql/updateTicketMutation.ts | 10 +++ apps/link/next.config.js | 6 -- apps/link/pages/_app.tsx | 37 +++++++++-- apps/link/pages/api/proxy/[[...path]].ts | 16 ++--- apps/link/pages/tickets/[id].tsx | 68 ++------------------ apps/link/pages/tickets/assigned.tsx | 2 +- docker-compose.yml | 24 ++++--- docker/elasticsearch/Dockerfile | 2 +- package-lock.json | 66 +++++++++++++++++++ 14 files changed, 251 insertions(+), 159 deletions(-) create mode 100644 apps/link/graphql/getTicketQuery.ts create mode 100644 apps/link/graphql/updateTicketMutation.ts diff --git a/apps/link/components/ArticleCreateDialog.tsx b/apps/link/components/ArticleCreateDialog.tsx index a3468c4..6cd47e4 100644 --- a/apps/link/components/ArticleCreateDialog.tsx +++ b/apps/link/components/ArticleCreateDialog.tsx @@ -1,6 +1,7 @@ import { FC, useState } from "react"; import { Grid, Button, Dialog, DialogActions, DialogContent, TextField } from "@mui/material"; -// import { request, gql } from "graphql-request"; +import { useSWRConfig } from "swr"; +import { updateTicketMutation } from "graphql/updateTicketMutation"; interface ArticleCreateDialogProps { ticketID: string; @@ -10,54 +11,25 @@ interface ArticleCreateDialogProps { } export const ArticleCreateDialog: FC = ({ ticketID, open, closeDialog, kind }) => { - console.log({ ticketID }) const [body, setBody] = useState(""); const backgroundColor = kind === "reply" ? "#1982FC" : "#FFB620"; const color = kind === "reply" ? "white" : "black"; - const origin = typeof window !== 'undefined' && window.location.origin - ? window.location.origin - : ''; + const { fetcher } = useSWRConfig(); const createArticle = async () => { - // const token = document?.querySelector('meta[name="csrf-token"]').getAttribute('content'); - // console.log({ token }) - const res = await fetch(`${origin}/api/v1/ticket_articles`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": "BG3wYuvTgi4ALfaZ-Mdq6i08wRFRJHeCPJbfGjfVarLRhwaxRC8J-AZvGiSNOiWrN38WT3C9WGLhcmaMb0AqBQ", - }, - body: JSON.stringify({ - ticket_id: ticketID, - body, - internal: kind === "note", - sender: "Agent", - }), - }); - console.log({ res }) - /* - const document = gql` - - mutation { - ticketUpdate( + await fetcher( + { + document: updateTicketMutation, + variables: { + ticketId: `gid://zammad/Ticket/${ticketID}`, input: { - ticketId: "1" - body: "This is a test article" - internal: false - } - ) { - article { - id + article: { + body, + type: kind === "note" ? "note" : "phone", + internal: kind === "note" + } } } - } - `; - const data = await request({ - url: `${origin}/graphql`, - document, - }); - - console.log({ data }) - */ + }); closeDialog(); setBody(""); } diff --git a/apps/link/components/TicketDetail.tsx b/apps/link/components/TicketDetail.tsx index 0ef66ed..be253c3 100644 --- a/apps/link/components/TicketDetail.tsx +++ b/apps/link/components/TicketDetail.tsx @@ -51,6 +51,7 @@ export const TicketDetail: FC = ({ ticket }) => { {ticket.articles.edges.map(({ node: article }: any) => ( = ({ ticket }) => { - const [selectedGroup, setSelectedGroup] = useState("group1"); - const [selectedOwner, setSelectedOwner] = useState("owner1"); - const [selectedPriority, setSelectedPriority] = useState("priority1"); - const [selectedState, setSelectedState] = useState("state2"); + const [selectedGroup, setSelectedGroup] = useState(1); + const [selectedOwner, setSelectedOwner] = useState(1); + const [selectedPriority, setSelectedPriority] = useState(1); + const [selectedState, setSelectedState] = useState(1); + const [selectedTags, setSelectedTags] = useState(["tag1", "tag2"]); const handleDelete = () => { console.info("You clicked the delete icon."); }; + const restFetcher = (url: string) => fetch(url).then((r) => r.json()); + const { data: groups } = useSWR("/api/v1/groups", restFetcher); + console.log({ groups }); + const { data: users } = useSWR("/api/v1/users", restFetcher); + console.log({ users }); + const { data: states } = useSWR("/api/v1/ticket_states", restFetcher); + console.log({ states }); + const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher); + console.log({ priorities }); + + const { fetcher } = useSWRConfig(); + const updateTicket = async () => { + await fetcher( + { + document: updateTicketMutation, + variables: { + ticketId: ticket.id, + input: { + ownerId: `gid://zammad/User/${selectedOwner}`, + tags: ["tag1", "tag2"], + } + } + }); + } return ( Group - { setSelectedGroup(e.target.value); updateTicket() }} size="small" sx={{ width: "100%", backgroundColor: "white" }} > - Default Group + {groups?.map((group: any) => {group.name})} Owner - { setSelectedOwner(e.target.value); updateTicket() }} size="small" sx={{ width: "100%", backgroundColor: "white", }} > - Darren Clarke - Darren Gpcmdln + {users?.map((user: any) => {user.firstname} {user.lastname})} @@ -43,11 +69,7 @@ export const TicketEdit: FC = ({ ticket }) => { width: "100%", backgroundColor: "white" }} > - closed - new - open - pending close - pending reminder + {states?.map((state: any) => {state.name})} @@ -56,17 +78,14 @@ export const TicketEdit: FC = ({ ticket }) => { width: "100%", backgroundColor: "white" }} > - 1 low - 2 normal - 3 high + {priorities?.map((priority: any) => {priority.name})} Tags - - + {selectedTags.map((tag: string) => )} diff --git a/apps/link/components/ZammadWrapper.tsx b/apps/link/components/ZammadWrapper.tsx index ecc60a5..64d426e 100644 --- a/apps/link/components/ZammadWrapper.tsx +++ b/apps/link/components/ZammadWrapper.tsx @@ -1,4 +1,5 @@ import { FC, useState } from "react"; +import { useRouter } from "next/router"; import Iframe from "react-iframe"; type ZammadWrapperProps = { @@ -10,11 +11,12 @@ export const ZammadWrapper: FC = ({ path, hideSidebar = true, }) => { + const router = useRouter(); const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - const [display, setDisplay] = useState("hidden"); + const [display, setDisplay] = useState("none"); const url = `${origin}/zammad${path}`; console.log({ origin, path, url }); @@ -49,7 +51,28 @@ export const ZammadWrapper: FC = ({ "display: none"; } + // @ts-ignore + if (linkElement.contentDocument.querySelector(".overview-header")) { + // @ts-ignore + linkElement.contentDocument.querySelector(".overview-header").style = + "display: none"; + } + setDisplay("inherit"); + + if (linkElement.contentWindow) { + linkElement.contentWindow.addEventListener('hashchange', () => { + const hash = linkElement.contentWindow?.location?.hash ?? "" + if (hash.startsWith("#ticket/zoom/")) { + setDisplay("none"); + const ticketID = hash.split("/").pop(); + router.push(`/tickets/${ticketID}`); + setTimeout(() => { + setDisplay("inherit"); + }, 1000); + } + }); + } } }} /> diff --git a/apps/link/graphql/getTicketQuery.ts b/apps/link/graphql/getTicketQuery.ts new file mode 100644 index 0000000..1965def --- /dev/null +++ b/apps/link/graphql/getTicketQuery.ts @@ -0,0 +1,40 @@ +import { gql } from 'graphql-request'; + +export const getTicketQuery = gql` +query getTicket($ticketId: ID!) { + ticket(ticket: { ticketId: $ticketId }) { + id + internalId + title + note + number + createdAt + updatedAt + closeAt + tags + state { + id + name + } + owner { + id + email + } + articles { + edges { + node { + id + body + internal + type { + name + } + sender { + name + } + } + } + } + } +} +`; diff --git a/apps/link/graphql/updateTicketMutation.ts b/apps/link/graphql/updateTicketMutation.ts new file mode 100644 index 0000000..26d8bd4 --- /dev/null +++ b/apps/link/graphql/updateTicketMutation.ts @@ -0,0 +1,10 @@ +import { gql } from "graphql-request"; + +export const updateTicketMutation = gql` +mutation UpdateTicket($ticketId: ID!, $input: TicketUpdateInput!) { + ticketUpdate(ticketId: $ticketId, input: $input) { + ticket { + id + } + } +}`; diff --git a/apps/link/next.config.js b/apps/link/next.config.js index 4eba182..854b848 100644 --- a/apps/link/next.config.js +++ b/apps/link/next.config.js @@ -1,11 +1,5 @@ module.exports = { rewrites: async () => ({ - beforeFiles: [ - { - source: "/#ticket/zoom/:path*", - destination: "/ticket/:path*", - }, - ], fallback: [ { source: "/:path*", diff --git a/apps/link/pages/_app.tsx b/apps/link/pages/_app.tsx index 7294b63..cb9c91b 100644 --- a/apps/link/pages/_app.tsx +++ b/apps/link/pages/_app.tsx @@ -1,4 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ +import { useState } from "react"; import { AppProps } from "next/app"; import { SessionProvider } from "next-auth/react"; import { CssBaseline } from "@mui/material"; @@ -13,6 +14,8 @@ import "@fontsource/roboto/700.css"; import "@fontsource/playfair-display/900.css"; import "styles/global.css"; import { LicenseInfo } from "@mui/x-data-grid-pro"; +import { SWRConfig } from "swr"; +import { GraphQLClient } from "graphql-request"; LicenseInfo.setLicenseKey( "fd009c623acc055adb16370731be92e4T1JERVI6NDA3NTQsRVhQSVJZPTE2ODAyNTAwMTUwMDAsS0VZVkVSU0lPTj0x" @@ -27,14 +30,40 @@ interface LinkWebProps extends AppProps { const LinkWeb = (props: LinkWebProps) => { const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; + const [csrfToken, setCsrfToken] = useState(""); + const origin = typeof window !== 'undefined' && window.location.origin + ? window.location.origin : null; + const client = new GraphQLClient(`${origin}/graphql`, { + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + }); + const graphQLFetcher = async ({ document, variables }: any) => { + const requestHeaders = { + "X-CSRF-Token": csrfToken, + }; + const { data, headers } = await client.rawRequest( + document, + variables, + requestHeaders + ); + + const token = headers.get('CSRF-Token'); + setCsrfToken(token); + + return data; + }; return ( - - - - + + + + + + ); diff --git a/apps/link/pages/api/proxy/[[...path]].ts b/apps/link/pages/api/proxy/[[...path]].ts index 5515383..2fa510a 100644 --- a/apps/link/pages/api/proxy/[[...path]].ts +++ b/apps/link/pages/api/proxy/[[...path]].ts @@ -13,19 +13,11 @@ const withAuthInfo = return res.redirect("/login"); } - try { - const res2 = await fetch(`http://127.0.0.1:8001`); - console.log({ res2 }) - } catch (e) { - console.log({ e }) + if (req.headers) { + req.headers['X-Forwarded-User'] = session.email.toLowerCase(); + req.headers['host'] = 'zammad.example.com'; } - console.log({ zammad: process.env.ZAMMAD_URL }) - req.headers['X-Forwarded-User'] = session.email.toLowerCase(); - req.headers['host'] = 'zammad.example.com'; - - console.log({ headers: req.headers }) - return handler(req, res); }; @@ -33,7 +25,7 @@ const proxy = createProxyMiddleware({ target: process.env.ZAMMAD_URL, changeOrigin: true, xfwd: true, - ws: true, + ws: false, pathRewrite: { "^/zammad": "" }, }); diff --git a/apps/link/pages/tickets/[id].tsx b/apps/link/pages/tickets/[id].tsx index 5d8ee09..502ff84 100644 --- a/apps/link/pages/tickets/[id].tsx +++ b/apps/link/pages/tickets/[id].tsx @@ -1,84 +1,24 @@ import { GetServerSideProps, GetServerSidePropsContext } from "next"; import Head from "next/head"; import useSWR from "swr"; -import { request, gql } from "graphql-request"; import { NextPage } from "next"; import { Grid } from "@mui/material"; import { Layout } from "components/Layout"; import { TicketDetail } from "components/TicketDetail"; import { TicketEdit } from "components/TicketEdit"; +import { getTicketQuery } from "graphql/getTicketQuery"; type TicketProps = { id: string; }; const Ticket: NextPage = ({ id }) => { - const origin = - typeof window !== "undefined" && window.location.origin - ? window.location.origin - : ""; - const graphQLFetcher = async ({ document, variables }: any) => { - const data = await request({ - url: `${origin}/graphql`, - document, - variables, - }); - console.log({ data }); - - return data; - }; - const { data: ticketData, error: ticketError }: any = useSWR( { - document: gql` - query getTicket($ticketId: Int!) { - ticket(ticket: { ticketInternalId: $ticketId }) { - id - internalId - title - note - number - createdAt - updatedAt - closeAt - articles { - edges { - node { - id - body - internal - sender { - name - } - } - } - } - } - } - `, - variables: { ticketId: parseInt(id, 10) }, + document: getTicketQuery, + variables: { ticketId: `gid://zammad/Ticket/${id}` }, }, - graphQLFetcher, - { refreshInterval: 1000 } - ); - - const { data: graphqlData2, error: graphqlError2 } = useSWR( - { - document: gql` - { - __schema { - queryType { - name - fields { - name - } - } - } - } - `, - variables: {}, - }, - graphQLFetcher + { refreshInterval: 100000 } ); const shouldRender = !ticketError && ticketData; diff --git a/apps/link/pages/tickets/assigned.tsx b/apps/link/pages/tickets/assigned.tsx index 45edf43..a176888 100644 --- a/apps/link/pages/tickets/assigned.tsx +++ b/apps/link/pages/tickets/assigned.tsx @@ -22,7 +22,7 @@ const Assigned: FC = () => ( width: "100%", }} > - + diff --git a/docker-compose.yml b/docker-compose.yml index c4c4d7b..4b9c48f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,12 +42,15 @@ services: container_name: zammad-elasticsearch environment: - discovery.type=single-node + - ES_JAVA_OPTS=-Xms750m -Xmx750m + - xpack.security.enabled=false build: ./docker/elasticsearch restart: ${RESTART} volumes: - elasticsearch-data:/usr/share/elasticsearch/data zammad-init: + platform: linux/x86_64 container_name: zammad-init command: [ "zammad-init" ] depends_on: @@ -68,6 +71,7 @@ services: restart: ${RESTART} zammad-nginx: + platform: linux/x86_64 container_name: zammad-nginx command: [ "zammad-nginx" ] expose: @@ -97,6 +101,7 @@ services: - postgresql-data:/var/lib/postgresql/data zammad-railsserver: + platform: linux/x86_64 container_name: zammad-railsserver command: [ "zammad-railsserver" ] depends_on: @@ -115,6 +120,7 @@ services: restart: ${RESTART} zammad-scheduler: + platform: linux/x86_64 container_name: zammad-scheduler command: [ "zammad-scheduler" ] depends_on: @@ -128,6 +134,7 @@ services: - zammad-data:/opt/zammad zammad-websocket: + platform: linux/x86_64 container_name: zammad-websocket command: [ "zammad-websocket" ] depends_on: @@ -173,7 +180,6 @@ services: # environment: *common-metamigo-variables metamigo-postgresql: - container_name: metamigo-postgresql build: ./docker/postgresql restart: ${RESTART} volumes: @@ -197,14 +203,14 @@ services: volumes: - ../signald:/signald - nginx-proxy: - container_name: nginx-proxy - build: ./docker/nginx-proxy - restart: ${RESTART} - ports: - - "80:80" - volumes: - - /var/run/docker.sock:/tmp/docker.sock:ro + # nginx-proxy: + # container_name: nginx-proxy + # build: ./docker/nginx-proxy + # restart: ${RESTART} + # ports: + # - "80:80" + # volumes: + # - /var/run/docker.sock:/tmp/docker.sock:ro link: container_name: link diff --git a/docker/elasticsearch/Dockerfile b/docker/elasticsearch/Dockerfile index 5415d31..74ea8f1 100644 --- a/docker/elasticsearch/Dockerfile +++ b/docker/elasticsearch/Dockerfile @@ -1 +1 @@ -FROM bitnami/elasticsearch:8.5.1 +FROM docker.elastic.co/elasticsearch/elasticsearch:8.6.2 diff --git a/package-lock.json b/package-lock.json index f65a418..ecd164e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11878,6 +11878,18 @@ "integrity": "sha512-vo76VJ44MkUBZL/BzpGXaKzMfroF4ZR6+haRuw9p+eSWfoNaH2AxVc8xmiEPC08jhzJSeM6w7/iMUGet8b4oBQ==", "peer": true }, + "node_modules/hapi/node_modules/b64": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/b64/-/b64-4.1.2.tgz", + "integrity": "sha512-+GUspBxlH3CJaxMUGUE1EBoWM6RKgWiYwUDal0qdf8m3ArnXNN1KzKVo5HOnE/FSq4HHyWf3TlHLsZI8PKQgrQ==", + "extraneous": true + }, + "node_modules/hapi/node_modules/big-time": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/big-time/-/big-time-2.0.1.tgz", + "integrity": "sha512-qtwYYoocwpiAxTXC5sIpB6nH5j6ckt+n/jhD7J5OEiFHnUZEFn0Xk8STUaE5s10LdazN/87bTDMe+fSihaW7Kg==", + "extraneous": true + }, "node_modules/hapi/node_modules/boom": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/boom/-/boom-7.2.2.tgz", @@ -11890,6 +11902,12 @@ "integrity": "sha512-1LPcXg3fkGVhjdA/P3DcR5cDktKEYtDpruJv9Nhmy36RoYaoxZfC82Zr2JmS3vysDJKqMtP0qJw3/P6iisTASg==", "peer": true }, + "node_modules/hapi/node_modules/bourne": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bourne/-/bourne-1.1.1.tgz", + "integrity": "sha512-Ou0l3W8+n1FuTOoIfIrCk9oF9WVWc+9fKoAl67XQr9Ws0z7LgILRZ7qtc9xdT4BveSKtnYXfKPgn8pFAqeQRew==", + "extraneous": true + }, "node_modules/hapi/node_modules/call": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/call/-/call-5.0.3.tgz", @@ -11908,6 +11926,18 @@ "integrity": "sha512-1tDnll066au0HXBSDHS/YQ34MQ2omBsmnA9g/jseyq/M3m7UPrajVtPDZK/rXgikSC1dfjo9Pa+kQ1qcyG2d3g==", "peer": true }, + "node_modules/hapi/node_modules/content": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/content/-/content-4.0.6.tgz", + "integrity": "sha512-lR9ND3dXiMdmsE84K6l02rMdgiBVmtYWu1Vr/gfSGHcIcznBj2QxmSdUgDuNFOA+G9yrb1IIWkZ7aKtB6hDGyA==", + "extraneous": true + }, + "node_modules/hapi/node_modules/cryptiles": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.3.tgz", + "integrity": "sha512-gT9nyTMSUC1JnziQpPbxKGBbUg8VL7Zn2NB4E1cJYvuXdElHrwxrV9bmltZGDzet45zSDGyYceueke1TjynGzw==", + "extraneous": true + }, "node_modules/hapi/node_modules/heavy": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/heavy/-/heavy-6.1.2.tgz", @@ -11920,18 +11950,42 @@ "integrity": "sha512-3PvUwBerLNVJiIVQdpkWF9F/M0ekgb2NPJWOhsE28RXSQPsY42YSnaJ8d1kZjcAz58TZ/Fk9Tw64xJsENFlJNw==", "peer": true }, + "node_modules/hapi/node_modules/iron": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/iron/-/iron-5.0.6.tgz", + "integrity": "sha512-zYUMOSkEXGBdwlV/AXF9zJC0aLuTJUKHkGeYS5I2g225M5i6SrxQyGJGhPgOR8BK1omL6N5i6TcwfsXbP8/Exw==", + "extraneous": true + }, "node_modules/hapi/node_modules/joi": { "version": "14.0.4", "resolved": "https://registry.npmjs.org/joi/-/joi-14.0.4.tgz", "integrity": "sha512-KUXRcinDUMMbtlOk7YLGHQvG73dLyf8bmgE+6sBTkdJbZpeGVGAlPXEHLiQBV7KinD/VLD5OA0EUgoTTfbRAJQ==", "peer": true }, + "node_modules/hapi/node_modules/mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "extraneous": true + }, "node_modules/hapi/node_modules/mimos": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/mimos/-/mimos-4.0.2.tgz", "integrity": "sha512-5XBsDqBqzSN88XPPH/TFpOalWOjHJM5Z2d3AMx/30iq+qXvYKd/8MPhqBwZDOLtoaIWInR3nLzMQcxfGK9djXA==", "peer": true }, + "node_modules/hapi/node_modules/nigel": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/nigel/-/nigel-3.0.4.tgz", + "integrity": "sha512-3SZCCS/duVDGxFpTROHEieC+itDo4UqL9JNUyQJv3rljudQbK6aqus5B4470OxhESPJLN93Qqxg16rH7DUjbfQ==", + "extraneous": true + }, + "node_modules/hapi/node_modules/pez": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pez/-/pez-4.0.5.tgz", + "integrity": "sha512-HvL8uiFIlkXbx/qw4B8jKDCWzo7Pnnd65Uvanf9OOCtb20MRcb9gtTVBf9NCnhETif1/nzbDHIjAWC/sUp7LIQ==", + "extraneous": true + }, "node_modules/hapi/node_modules/podium": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/podium/-/podium-3.1.5.tgz", @@ -11974,6 +12028,18 @@ "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", "peer": true }, + "node_modules/hapi/node_modules/vise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vise/-/vise-3.0.1.tgz", + "integrity": "sha512-7BJNjsv2o83+E6AHAFSnjQF324UTgypsR/Sw/iFmLvr7RgJrEXF1xNBvb5LJfi+1FvWQXjJK4X41WMuHMeunPQ==", + "extraneous": true + }, + "node_modules/hapi/node_modules/wreck": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/wreck/-/wreck-14.1.3.tgz", + "integrity": "sha512-hb/BUtjX3ObbwO3slCOLCenQ4EP8e+n8j6FmTne3VhEFp5XV1faSJojiyxVSvw34vgdeTG5baLTl4NmjwokLlw==", + "extraneous": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",